Less code, more Imutable - Immer 介绍 + 源码解读
介绍
immer 是 mobX 的作者编写的简化操作 immutable 数据的库。immer 通过使用 Proxy 元编程,使得我们能以一种更简单清晰的方式去处理 immutable 特性的数据。
例子
最直接的好处就是使得我们的代码变得清晰,比如在编写 redux reducer 时,我们需要保证返回新的 state 对象,如果使用深克隆在性能上会带来损耗,如果使用 … 运算符,在面对复杂结构时,则会使得代码变得复杂。
通过 immer 我们可以使用一种清晰易懂的方式编写类似的代码。
import produce from "immer"
const current = [
{
todo: "Learn typescript",
done: true
},
{
todo: "Try immer",
done: false
}
];
// 不使用 immer
const next = [
...current.map((todo, index) => index === 1 ? {
...todo, done: true} : todo),
{
todo: "Tweet about it"},
]
// 在使用了 immer 后
const next = produce(current, draft => {
draft.push({
todo: "Tweet about it", done: false });
draft[1].done = true;
});
**
reducer
**
实际上 immer 经常被配合 Redux 的 reducer 一起使用,可以大大简化我们编写 reducer 的代码逻辑
// Redux reducer
// Shortened, based on: https://github.com/reactjs/redux/blob/master/examples/shopping-cart/src/reducers/products.js
const reducer = (state = {
}, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
return {
...state,
...action.products.reduce((obj, product) => {
obj[product.id] = product
return obj
}, {
})
}
default:
return state
}
}
// 使用 immer
import produce from "immer"
const reducer = (state = {
}, action) => produce(state, (draft) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
action.products.forEach(product => {
draft[product.id] = product
})
}
}, {
})
**
使用
immer 只有一个核心函数 produce ,第一个参数是我们想要改变的 current 对象,第二个参数是一个函数,这个函数接受一个 draft 对象,而这个 draft 对象,其实就是 produce 内部使用 Proxy 包装后的 current 对象。
我们在函数中通过 draft 修改 current 上的数据,immer 通过 Proxy 拦截,收集我们作出的修改,最终根据这些修改来计算返回一个新的 state 对象。原来 state 对象上的所有数据则保持原样。
**
原理
**
我们传入的 current 会在 immer 内部使用 Proxy 进行包装,而 draft 就是包装后的 current 对象,我们修改 draft 上的属性时,immer 内部会拦截我们的修改操作,使得变动不会反应到原来的 current 对象上,最后返回一个产生变化的新的 next 对象。
结构共享
不同于深克隆的是,immer 返回的数据是 结构共享 的 。也就是说 current 和 next 共享未改变的部分,减少了不必要的克隆操作带来的额外性能问题。
**
由于蓝色部分并未发生变动,所以在结构上进行共享。注意,结构共享只是一种内部优化手段,从语义上我们可以看作返回了一个全新的对象。
const current = {
name: 'state',
a: {
name: 'a',
b: {
name: 'b' },
c: {
name: 'c',
d: {
name: 'd' },
e: {
name: 'e' },
}
}
}
// 如果我们手动的写
const next = {
...current,
a: {
...crrent.a,
c: {
...current.c,
name: 'new c',
},
},
};
// 使用 immer
const next = produce(current, draft => {
draft.a.c.name = 'new c';
});
console.log(next === current); // => false
console.log(next.a === current.a); // => false
console.log