Immer – Create the next immutable state by mutating the current one
Immer is a way to create immutable objects by mutate the current object. For example, when we use React, we often encounter the problem of not updating memo or unnecessary updates. Immer can be used to solve the problem
const Profile = memo((props) = > {
console.log("render <Profile />");
return (
<>
<div>Name: {props.user.name}</div>
<div>Age: {props.user.age}</div>
</>
);
});
const Blog = memo((props) = > {
console.log("render <Blog />");
return <div>blog: {props.blog.content}</div>;
});
class User extends Component {
state = {
user: {
name: "Michel".age: 33
},
blog: {
content: "hahah..."}};render() {
return (
<div>
<Profile user={this.state.user} />
<Blog blog={this.state.blog} />
<button onClick={this.onUpdateAgeByManual0}>onUpdateAgeByManual0</button>
<button onClick={this.onUpdateAgeByManual1}>onUpdateAgeByManual1</button>
<button onClick={this.onUpdateAgeByManual2}>onUpdateAgeByManual2</button>
<button onClick={this.onUpdateAgeByImmer}>onUpdateAgeByImmer</button>
</div>
);
}
onUpdateAgeByManual0 = () = > {
this.setState((prevState) = > {
prevState.user.age++;
return {
user: { ...prevState.user },
blog: prevState.blog
};
});
};
onUpdateAgeByManual1 = () = > {
this.setState((prevState) = > {
prevState.user.age++;
return { ...prevState };
});
};
onUpdateAgeByManual2 = () = > {
this.setState((prevState) = > {
prevState.user.age++;
return {
user: { ...prevState.user },
blog: { ...prevState.blog }
};
});
};
onUpdateAgeByImmer = () = > {
this.setState(
produce(this.state, (draft) = > {
draft.user.age += 1; })); }; }Copy the code
Can you guess how the onUpdate EByManual will be updated?
Click me to show the answer…
- OnUpdateAgeByManual0: Ideal update status that updates only Profile components
- OnUpdateAgeByManual1: There will be no component updates, the reference to the User object will not change, and the memo will not be updated after a shallow comparison
- OnUpdateAgeByManual2: causes unnecessary component updates, changes in references to the blog object, and blog component updates triggered after a shallow memo comparison
- OnUpdateAgeByImmer: with onUpdateAgeByManual0
So what immutable guarantees is that a new object is created every time it is updated. Deep copy is fine, but for performance reasons, you need to make sure that only the properties of the object that have changed generate new references, and the other properties that have not changed still use the old references, which is what Immer does
The principle of
In the words of the author:
- Copy on write
- Proxies
The work of Produce is divided into three stages, namely create proxy (createDraft), modify proxy (produceDraft), finalize (Finalize). What create proxy does is to proxy the first parameter base object passed in. That is, when the callback is passed in, it can perform the ShallowCopy on write operation, which ultimately points a reference to the modified object to the ShallowCopy object
So the key principle is how do you implement ShallowCopy on Write and how do you proxy
ShallowCopy on write
ShallowCopy on Write works like the one shown above. Here’s an example to understand the process
const state = {
user: {
name: "Michel".age: 33
},
blog: {
content: "hahah..."}};const nextState = produce(state, (draft) = > {
draft.user.age = draft.user.age + 1;
});
Copy the code
An object is structured like a tree, with properties of the primitive type acting as leaf nodes
First of all, when reading, that is, when the get operation of the object is triggered, the value will be returned according to the type of the value. If the value is a basic type, it means that the leaf node is accessed and can be returned directly. If it is a reference type, continue to create a proxy for that value, implementing a “lazy proxy.” In the draft.user.age + 1 example, the user on draft is read first, and the proxy of user is returned. Then the age property on user is read, and the basic type 33 is returned
After the agent is created for the first time, it is stored. In this way, when the agent is read again, it can directly return the stored agent, reducing the overhead of creating the agent and storing the operation on the agent. In this way, the operation on the agent will not be invalid when the agent is created every time. In this example, the part before the draft.user.age = set operation is equivalent to reading again to get the previous agent
Then, when modified, a shallow copy is created, and the parent node makes a shallow copy and changes on the shallow copy objects, while Object.assign(state.copy, state.drafts) re-stores the previous agent on the shallow copy objects, again ensuring that the previous actions are valid. In this example, the draft.user.age = set operation triggers the set operation of the user agent, creates a shallow copy of user, then creates a shallow copy of Draft, and modifies the age attribute of the user shallow copy object
When finalize, the object tree is traversed. If the proxy is changed, the shallow copy object is returned. If no changes are made, only the original object is returned
Proxies
Immer uses the Proxy API to delegate objects and arrays. Map and Set are created by DraftMap and DraftSet overrides Set, GET, add, delete, and has. The ES5 environment uses defineProperty to Proxy objects and arrays, so the Proxy API is not necessary, just to facilitate the operation of the Proxy object, we can also specify some methods to implement the Proxy operation
implementation
The implementation of about 100 lines can be seen in AHABHgK/simple-IMmer. Immer V1.0.0 has made some simplification, the principle is clearer, and supports Corrification and asynchronization. There will be annotations for difficult points, as well as relatively complete tests, so it is easy to get started and debug
In addition, since Immer supports Currization, it is easy to implement useImmer, which can be viewed directly on Github
Plan to change the writing style, before the three Vue3 articles have large sections of paste code, smelly and long, will only hypnotize it, later to write will try to explain the principle, write concise, in addition to very refined will paste code, other times the source directly to the link, there are difficult points will be written through annotations and tests