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)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?