the beginning of the story,
immer.js
should be a library that became popular in 2019, it can efficiently copy an object (for example, relative to JSON.parse(JSON.stringify(obj))
), and freeze the modification permissions of some values on this object .
But I found that some classmates respected the large-scale use of immer.js
in the project to manipulate objects, but they did not give a reason for me to agree with his approach, so I decided to study immer.js
principle, I hope it can be used better and more accurately immer.js
.
Don't use it for the sake of use, it may be the key to learn and understand the idea of a certain technology and apply it in the right place. Today, we will analyze it from the perspective of principle immer.js
suitable usage scenarios.
1. What's wrong with copying objects?
1: Simplest copy
The following object needs to be copied, and change the name
attribute to 金毛2
:
const obj1 = {
name: '金毛1',
city: '上海'
}
To copy directly:
const obj2 = obj1;
obj2.name = '金毛2';
console.log(obj1) // {name:'金毛2', city:'上海'}
The above is the simplest example, and the reason is well known because direct const obj2 = obj1;
belongs to direct let obj2
the address points to obj1
.
So if you don't want every modification obj2
affect obj1
, then we can make a deep copy of obj1
and play with it:
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.name = '金毛2'
console.log(obj1) // {name: '金毛1', city: '上海'}
console.log(obj2) // {name: '金毛2', city: '上海'}
2: larger objects
In the actual project, the object to be manipulated may be far more complex than the example, such as city
If it is a huge object, then when JSON.parse(JSON.stringify(obj1))
will waste a lot of performance in copying city
property, but we might just want a name
different object.
As you may quickly think, destructuring directly with the spread operator:
const obj1 = {
name: '金毛1',
city: {
'上海': true,
'辽宁': false,
'其他城市': false
}
}
const obj2 = { ...obj1 }
obj2.name = '金毛2'
console.log(obj1.city === obj2.city)
3: For multi-layered objects
For example, the name attribute is a multi-level nested type, at this time we only want to change the value of the inner basename: 2022
used name
:
const obj1 = {
name: {
nickname: {
2021: 'cc_2021_n',
2022: 'cc_2022_n'
},
basename: {
2021: 'cc_2021',
2022: 'cc_2022'
}
},
city: { '上海': true }
}
const obj2 = { ...obj1 }
obj2.name = {
...obj1.name
}
obj2.name.basename = {
...obj1.name.basename
}
obj2.name.basename['2022'] = '金毛2'
console.log(obj1.name.basename === obj2.name.basename) // false
console.log(obj1.name.nickname === obj2.name.nickname) // true
console.log(obj1.city === obj2.city) // true
In the above code, we need to repeatedly deconstruct the object in order to reuse city
and nickname
and other objects, but if we not only want to modify obj2.name.basename['2022']
, but How to modify a variable in an object n
? At this time, we need a plug-in to help us encapsulate these tedious steps.
2. Basic capabilities of immer.js
Let's directly demonstrate the usage of immer.js
to see how elegant it is:
1: Install
yarn add immer
2: use
immer
provides the produce
method, the second parameter received by this method draft
is the same value as the first parameter, the magic is in produce
方法内操作draft
会被记录下来, 比如---c001dff3f7427b62c47ed3cf7ed2b50c draft.name = 1
obj2
的name
becomes 1 and the name
attribute becomes obj2独有的
:
const immer = require('immer')
const obj1 = {
name: {
nickname: {
2021: 'cc_2021_n',
2022: 'cc_2022_n'
},
basename: {
2021: 'cc_2021',
2022: 'cc_2022'
}
},
city: { '上海': true }
}
const obj2 = immer.produce(obj1, (draft) => {
draft.name.basename['2022'] = '修改name'
})
console.log(obj1.name.basename === obj2.name.basename) // false
console.log(obj1.name.nickname === obj2.name.nickname) // true
console.log(obj1.city === obj2.city) // true
3: Specify 2 values
We also make a modification to city
, so only nickname
attributes are reused at this time:
const immer = require('immer')
const obj1 = {
name: {
nickname: {
2021: 'cc_2021_n',
2022: 'cc_2022_n'
},
basename: {
2021: 'cc_2021',
2022: 'cc_2022'
}
},
city: { '上海': true }
}
const obj2 = immer.produce(obj1, (draft) => {
draft.name.basename['2022'] = '修改name'
draft.city['上海'] = false
})
console.log(obj1.name.basename === obj2.name.basename) // false
console.log(obj1.name.nickname === obj2.name.nickname) // true
console.log(obj1.city === obj2.city) // false
3. Analysis of plug-in requirements
Let's briefly sort out what we have done immer
and see what difficulties we have to overcome:
-
produce
The first parameter of the method passes in the object to be copied. - The second parameter of the
produce
method is a function, which records all the pairsdraft
赋值操作
. - The assigned object will generate a new object to replace the corresponding value on the body of
obj2
. - Operations on
draft
will not affect the first parameter of theproduce
method. - If it has not been processed
nickname
, it will be reused directly.
Fourth, the core principle
produce
The draft
3eaca4aecf7552aeba90b797c8ea0783--- in the method is obviously a proxy object, then we can use the new Proxy
method to generate the proxy object, and use get 与 set
method to know which variables were changed.
Take the data structure in our example as an example:
const obj1 = {
name: {
nickname: {
2021: 'cc_2021_n',
2022: 'cc_2022_n'
},
basename: {
2021: 'cc_2021',
2022: 'cc_2022'
}
},
city: { '上海': true }
}
obj1.name.basename[2022]
, 那么basename
的key( 2022
)的指向, basename
Change, he is not the original basename
let's call him basename改
.
basename
的父级是name
, 那name
的basename
不能继续使用原本的basename
, but should point to basename改
, so the attribute name
has also changed.
And so on. In fact, as long as we modify a value, the parent with this value will also be modified. If the parent is modified, the parent of the parent will also be modified, forming a 修改链
, So it may be necessary to use 回溯算法
for step-by-step modifications.
There is only one core goal, only the changed variables are newly created, and the rest of the variables are reused!
Five, the basic core code
First of all, the code I wrote by myself is a bit ugly, so what I am demonstrating here is written after reading many articles and videos. This version is only true Object & Array
two data types, the principle is to understand Can:
1: Write some basic tools and methods
const isObject = (val) => Object.prototype.toString.call(val) === '[object Object]';
const isArray = (val) => Object.prototype.toString.call(val) === '[object Array]';
const isFunction = (val) => typeof val === 'function';
function createDraftstate(targetState) {
if (isObject) {
return Object.assign({}, targetState)
} else if (isArray(targetState)) {
return [...targetState]
} else {
return targetState
}
}
-
createDraftstate
method is a shallow copy method.
2: Entry method
function produce(targetState, producer) {
let proxyState = toProxy(targetState)
producer(proxyState);
return // 返回最终生成的可用对象
}
-
targetState
is the object to be copied, which isobj1
in the above example. -
producer
is the processing method passed in by the developer. -
toProxy
is a method to generate a proxy object, which is used to record the user's assignment to those attributes. - Finally, a copied object can be returned, and the specific logic here is a bit 'circumstance' to be discussed later.
3: Core proxy method toProxy
The core capability of this method is right 操作目标对象的记录
, the following is the basic method structure demonstration:
function toProxy(targetState) {
let internal = {
targetState,
keyToProxy: {},
changed: false,
draftstate: createDraftstate(targetState),
}
return new Proxy(targetState, {
get(_, key) {
},
set(_, key, value) {
}
})
}
-
internal
详细的记录下每个代理对象的各种值, 比如---02435205d2c73ab679e09e63c40064a5obj2.name
会生成一个自己的internal
,obj2.name.nickname
It will also generate one of its owninternal
, which is a bit abstract here, everyone. -
targetState
: The original value is recorded, that is, the incoming value. -
keyToProxy
: record whichkey
was read (not modified), and the corresponding value ofkey
. -
changed
: Whether thekey
value of the current loop has been modified. -
draftstate
: A shallow copy of the current value of this ring.
4: get and set methods
Set a global one for internal use key
, which is convenient for subsequent values:
const INTERNAL = Symbol('internal')
get and set methods
get(_, key) {
if (key === INTERNAL) return internal
const val = targetState[key];
if (key in internal.keyToProxy) {
return internal.keyToProxy[key]
} else {
internal.keyToProxy[key] = toProxy(val)
}
return internal.keyToProxy[key]
},
set(_, key, value) {
internal.changed = true;
internal.draftstate[key] = value
return true
}
get method:
-
if (key === INTERNAL)return internal
: This is for the subsequent use of thiskey
to get theinternal
instance. - Each time the value is taken, it will be judged whether this
key
has a corresponding proxy attribute, if not, recursively use thetoProxy
method to generate a proxy object. - The final return is the proxy object
set method:
- Every time we use
set
even the same assignment we think has changed,changed
attribute becomestrue
. -
draftstate[key]
that is, the value of its own shallow copy, which has become the value assigned by the developer. - The final generated
obj2
is actually composed of alldraftstate
.
5: Backtracking method, change full link parent
The above code is only the most basic modification of a value, but as mentioned above, if a value changes, it will generate 修改链
from the beginning to the parent, then let's write the backtracking method:
The outermost layer will accept a backTracking
method:
function toProxy(targetState, backTracking = () => { }) {
Internally, methods are used and defined:
get(_, key) {
if (key === INTERNAL) {
return internal
}
const val = targetState[key];
if (key in internal.keyToProxy) {
return internal.keyToProxy[key]
} else {
internal.keyToProxy[key] = toProxy(val, () => {
internal.changed = true;
const proxyChild = internal.keyToProxy[key];
internal.draftstate[key] = proxyChild[INTERNAL].draftstate;
backTracking()
})
}
return internal.keyToProxy[key]
},
set(_, key, value) {
internal.changed = true;
internal.draftstate[key] = value
backTracking()
return true
}
Inside get:
- Each call to
toProxy
generates a proxy object and passes a method. If this method is triggered,changed
is changed totrue
, that is, the record itself is modified status. -
proxyChild
: Get the changed subset. -
internal.draftstate[key] = proxyChild[INTERNAL].draftstate;
: Assign the modified value of the subset to itself. -
backTracking()
: Because its own value has changed, make its own parent do the same.
Inside set:
-
backTracking()
: Execute the method passed in by the parent, forcing the parent to change and pointingkey
to its new value which isdraftstate
.
6: Principle combing
脱离代码总的来说一下原理吧, 比如我们取obj.name.nickname = 1
, 则会先触发obj
身上的get
方法, obj.name
的keyToProxy
上, obj.name
的get
方法, 为---983c626c32caaf4eb59dccc702b555c0 obj.name.nickname
Mounted on keyToProxy
, and finally obj.name.nickname = 1
triggered obj.name.nickname
the set
method.
set
方法触发backTracking
触发父级的方法, 父级将子元素的值draftstate
对应的key
.
All the proxy objects are in keyToProxy
, but the last one returned is draftstate
so there will be no multi-layer Proxy
case ('Matryoshka proxy').
7: Complete code
const INTERNAL = Symbol('internal')
function produce(targetState, producer) {
let proxyState = toProxy(targetState)
producer(proxyState);
const internal = proxyState[INTERNAL];
return internal.changed ? internal.draftstate : internal.targetState
}
function toProxy(targetState, backTracking = () => { }) {
let internal = {
targetState,
keyToProxy: {},
changed: false,
draftstate: createDraftstate(targetState),
}
return new Proxy(targetState, {
get(_, key) {
if (key === INTERNAL) {
return internal
}
const val = targetState[key];
if (key in internal.keyToProxy) {
return internal.keyToProxy[key]
} else {
internal.keyToProxy[key] = toProxy(val, () => {
internal.changed = true;
const proxyChild = internal.keyToProxy[key];
internal.draftstate[key] = proxyChild[INTERNAL].draftstate;
backTracking()
})
}
return internal.keyToProxy[key]
},
set(_, key, value) {
internal.changed = true;
internal.draftstate[key] = value
backTracking()
return true
}
})
}
function createDraftstate(targetState) {
if (isObject) {
return Object.assign({}, targetState)
} else if (isArray(targetState)) {
return [...targetState]
} else {
// 还有很多类型, 慢慢写
return targetState
}
}
module.exports = {
produce
}
Six, immer's code is somewhat irregular
Originally I wanted to use immer.js
as the source code to display, but the source code has made a lot of compatible es5
code, the readability is poor, and the code specification does not meet the requirements, it is easy To give you an example of mistakes, let's take a look at a few irregular points:
1: Variable information is not semantic
This kind of digital transmission is really incomprehensible. In fact, it corresponds to the error code:
In fact, in terms of readability, you should at least write enum
:
2: Super ternary 'can't stop'
This is not much to say, it looks too 'top'.
3: It's all any, does this ts still make sense...
7. Show off the interview experience on the ground
深拷贝
and 浅拷贝
belong to primary 八股文
, but if you give the interviewer a spin on the spot, it is estimated that you have finished writing a copy of immer.js
. The interviewer's silence is left, just raise this question by 2 levels, let's teach him, a new storm has appeared!
Eight, the ability to freeze data: setAutoFreeze
method
immer.js
There is also an important ability, that is, the freezing attribute prohibits modification.
We can see from the picture that modifying the value of obj2
cannot take effect unless the immer
instance on the setAutoFreeze
method is used:
Of course, continue to use the immer
method to modify the value:
Nine, special circumstances 'big battle'
The example we wrote only has the core functions, but we can try it together immer.js
it is not rigorous enough, so the following code immer
is the source code and not written by us.
1: the value does not change
We have a value equal to itself and see if it changes:
Although the set
method is triggered, the passed-in object is still returned.
2: function function and function again
After the function is executed, the return value changes
No new object is returned without changing the value:
3: pop is a nonsensical modification
pop()
can make the array change, but not triggered set
method, what is the effect of this:
Although it does not trigger set
, it will trigger the processing of functions in get
.
X. immer.js
Limitations
We have basically understood the working principle of immer.js
, then you can actually feel that it is not necessary to use immer.js
Proxy
ordinary business development. Proxy
also consumes performance.
Before using it, you can look up the amount you want to change to see how much the object remains unchanged after copying the object. It may be that the performance saved is really not much.
Although the experience of immer.js
has been very good, there are still some learning costs.
But if the scene you are facing is 大&复杂
then immer.js
is indeed a good choice, such as react
the performance problem of the source code, the rendering problem of the map, etc.
Eleven, the use of react
This article is based on the principle of immer.js
, so I put the react
related here, for example, we useState
declare a relatively deep object:
function App() {
const [todos, setTodos] = useState({
user: {
name: {
nickname: {
2021: 'cc_2021',
2022: 'cc_2022'
}
},
age: 9
}
});
return (
<div className="App" onClick={() => {
// 此处编写, 更改nickname[2022] = '新name'
}}>
{
todos.user.name.nickname[2022]
}
</div>
);
}
Method 1: full copy
const _todos = JSON.parse(JSON.stringify(todos));
_todos.user.name.nickname[2022] = '新的';
setTodos(_todos)
Now that everyone sees JSON.parse(JSON.stringify(todos))
does this model think of our immer.js
.
Method 2: Destructuring assignment
const _todos = {
user: {
...todos.user,
name: {
...todos.user.name,
nickname: {
...todos.user.name.nickname,
2022: '新的'
}
}
}
};
setTodos(_todos)
Method 3: The new variable triggers the update
Newly declare a variable, which is responsible for triggering the refresh mechanism of react
:
const [_, setReload] = useState({})
Every change todos
will not trigger react
refresh, and when setTodos
react's judgment mechanism thinks that the value has not changed and will not be refreshed, so other hooks are needed to trigger a refresh:
todos.user.name.nickname[2022] = '新的';
setTodos(todos)
setReload({})
Method 4: immer.js
Trigger refresh
Install:
yarn add immer
Introduce:
import produce from "immer";
use:
setTodos(
produce((draft) => {
draft.user.name.nickname[2022] = '新的';
})
);
setTodos
method receives the function, then the function is executed and the parameter is the todos
variable, immer.js
the first parameter in the source code is the function and the relevant conversion processing is done:
But I still feel that it is better to have a separate entry method, the logic is all placed in produce
it feels a bit messy, and when reading the source code directly, it will feel inexplicable!
Method 5: immer.js
provided hooks
Install:
yarn add use-immer
Introduce:
import { useImmer } from "use-immer";
use:
// 这里注意用useImmer代替useState
const [todos, setTodos] = useImmer({
user: {
name: {
nickname: {
2021: 'cc_2021',
2022: 'cc_2022'
}
},
age: 9
}
});
// 使用时:
setTodos((draft) => {
draft.user.name.nickname[2022] = '新的';
}
13. Inspiration
The two articles I wrote recently are about how to optimize the technology to the extreme. The last article is how does the Qwik.js framework pursue extreme performance?! I deeply feel that some of the codes that I take for granted exist in the way of writing. The point that can be optimized to the extreme, sometimes writing code is like boiling a frog in warm water, and you get used to it when you write it. Can we think of some ways to make ourselves think outside the box and re-examine our abilities?
end
That's it this time, hope to progress with you.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。