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的更新则只改变更新的部分结构。

image

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通过拦截取值操作和赋值操作将变化前后状态都记录下来。
image

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)
posted @   Tommy_marc  阅读(652)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示