Immer简单解析
immer基础介绍
github主页 https://github.com/immerjs/immer
文档 https://immerjs.github.io/immer
为什么immer让人眼前一亮
immer是一个简单易用的immutable结构生成库,在react的生态里,需要通过immutable来驱动组件更新,然而immutable数据生成的操作较为繁琐。
const state = {
a: {
x: 1,
y: 2,
},
b: {
x: 2,
y: 4,
}.
};
// 将a的x改为2
const newState = {
...state,
a: {
...state[a],
x: 2,
},
};
相比mutable的操作,immutable的操作就繁琐得多,数据结构复杂起来,可能出更多的问题。immer却能用mutable的操作生成immutable的数据。
import { produce } from 'immer';
const newState = produce(state, draft => {
draft.a.x = 2;
});
简单又优雅,棒~
其他解决方案——immutablejs、deepClone
没有immer前,有两个比较流行的方案immutablejs、deepClone。先说deepClone吧,就是把对象深度拷贝一遍。优点是简单粗暴,缺点是性能差,同时原本数据没变的组件也会重新渲染。immer的更新则只改变更新的部分结构。
immutable.js则是一套比较重的方案,体积大概20+kb,有自己的一套api。简单来说,就是先把数据转化为自己的类型对象,再通过一系列api进行操作。优点是性能高、api操作高效(这里指代码短),缺点是代码侵入性强、重、对ts支持不够完善。
const { Map } = require('immutable');
const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = Map({ a: 1, b: 2, c: 3 });
map1.equals(map2); // true
map1 === map2; // false
总结
immer简直完美。
immer的两板斧——produce的两种用法
immer也有一些高级的玩法,比如脏值记录,但produce的两种玩法足够覆盖99%的需求了。
最基础的玩法就是直接生成新的对象
import produce from "immer"
const baseState = [
{
todo: "Learn typescript",
done: true
},
{
todo: "Try immer",
done: false
}
]
const nextState = produce(baseState, draftState => {
draftState.push({todo: "Tweet about it"})
draftState[1].done = true
})
没有改过的对象是不变的
// the new item is only added to the next state,
// base state is unmodified
expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)
// same for the changed 'done' prop
expect(baseState[1].done).toBe(false)
expect(nextState[1].done).toBe(true)
// unchanged data is structurally shared
expect(nextState[0]).toBe(baseState[0])
// changed data not (dûh)
expect(nextState[1]).not.toBe(baseState[1])
柯里化
produce函数支持柯里化。
import produce from "immer"
const INITIAL_STATE = {
x: 3,
y: 4,
}
const precess = producer(draft => {
draft.x = draft.x + 1;
});
甚至可以直接变成reducer
import produce from "immer"
// Reducer with initial state
const INITIAL_STATE = {}
const byId = produce((draft, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
action.products.forEach(product => {
draft[product.id] = product
})
break
}
}, INITIAL_STATE)
immer源码解析
原理解析
produce函数的逻辑很简单,不考虑柯里化可以看作三个步骤:生成draft对象,对draft对象进行修改操作,生成新的结果。
function produce(baseState, recipe) {
const draft = createProxy(baseState);
recipe(draft);
const result = finalize(draft);
}
其中关键在于生成的draft对象,draft通过拦截取值操作和赋值操作将变化前后状态都记录下来。
finalize是一个递归的过程,如果modified属性没变返回原对象,否则遍历所有子属性,如果是值类型返回改变后的值,如果是对象重复上面步骤。最后会生成一个最小变化的新对象。
找了个demo,便于理解
Demo 仅用作理解原理,不支持监听数组
function produce (base, producer) {
const baseProxy = createProxy(undefined, base) /* 创建代理 */
producer.call(baseProxy, baseProxy) /* 执行 mutable 操作 */
return finalize(baseProxy) /* 递归合成新对象 */
}
function createState (parent, base) { /* base 对象代理 */
return {
modified: false, /* 表示该对象是否已经被更改 */
base, /* 原始对象*/
parent,
copy: undefined, /* 对原始对象的改动最终都会在这里保存一份 */
proxies: {} /* 存储对象子节点的代理 */
}
}
function markChanged (state) {
state.modified = true;
state.copy = Object.assign(Object.create(null), state.base)
Object.assign(state.copy, state.proxies)
if (state.parent) markChanged(state.parent)
}
const _STATE_ = '_______'
function createProxy (parent, base) {
const state = createState(parent, base)
const proxy = Proxy.revocable(state, {
get: function (state, prop) {
if (prop === _STATE_) return state /* 如果能通过 _STATE_ 拿到数据,表明它是一个 proxy */
if (state.modified) {
const value = state.copy[prop]
/* 只有 base 的才有必要去 proxy,否则如果是新增的的对象则直接返回 */
if (value === state.base[prop] && toString.call(value) === '[object Object]') {
state.copy[prop] = createProxy(state, value)
return state.copy[prop]
}
return value
} else {
/* 如果没有改动过数据,那仅仅是创建一个代理 */
if (state.proxies[prop]) return state.proxies[prop]
const value = state.base[prop]
if (toString.call(value) === '[object Object]') {
state.proxies[prop] = createProxy(state, value)
return state.proxies[prop]
}
return value
}
},
set: function (state, prop, value) {
if (!state.modified) {
markChanged(state)
}
state.copy[prop] = value /* 修改对象都在 copy 上执行 */
}
})
return proxy.proxy
}
function finalize(rootProxy) {
if (!!rootProxy && rootProxy[_STATE_]) {
const state = rootProxy[_STATE_]
for (let key in state.copy) {
if (state.copy[key] !== state.base[key]) {
state.copy[key] = finalize(state.copy[key])
}
}
return state.copy
} else {
return rootProxy
}
}
const a = { b: 1 }
const c = { d: 2 }
const state = {
a: a,
c: c,
d: {
c: { e: 4 },
f: 2
}
}
var state2 = produce(state, draft => {
draft.a.b = 21222
draft.a.f = 3
})
console.log(state2.d === state.d, true)
console.log(state2 === state, false)
console.log(state2.a.f, 3)
console.log(state.a.b, 1)
console.log(state2.a.b, 21222)