react-zustand-immer

7 min read
Table of Contents

核心概念

1. 三个关键角色

  • Base State(基础状态):原始的不可变数据
  • Draft State(草稿状态):可以被修改的代理对象
  • Next State(下一个状态):基于修改生成的新的不可变数据

2. 基本使用方式

javascript
const newState = produce(baseState, (draft) => {
// 在这里可以直接修改 draft
draft.user.name = '新名字'
})

实现原理

简化版实现

基于项目中的示例代码,我们可以看到 Immer 的核心实现:

javascript
const produce = (base, fn) => {
// 1. 用于存储所有修改的对象
const modified = {}
// 2. 创建 Proxy 处理器
const handler = {
// 拦截读取操作
get(target, prop, receiver) {
// 如果属性已被修改,返回修改后的值
if (prop in modified) {
return modified[prop]
}
// 如果属性是对象,递归创建 Proxy
if (typeof target[prop] === 'object' && target[prop] !== null) {
return new Proxy(target[prop], handler)
}
// 否则返回原始值
return Reflect.get(target, prop, receiver)
},
// 拦截写入操作
set(_target, prop, value) {
// 将修改记录到 modified 对象中
return Reflect.set(modified, prop, value)
},
}
// 3. 创建基础对象的代理
const proxy = new Proxy(base, handler)
// 4. 执行用户的修改函数
fn(proxy)
// 5. 如果没有修改,直接返回原对象
if (Object.keys(modified).length === 0) {
return base
}
// 6. 返回合并后的新对象
return JSON.parse(JSON.stringify(proxy))
}

工作流程详解

步骤 1:创建 Proxy

当调用 produce(obj, fn) 时,Immer 会为基础对象创建一个 Proxy 代理。

javascript
const obj = {
user: {
name: '张三',
age: 18,
},
}
const newObj = produce(obj, (draft) => {
// draft 是 obj 的 Proxy 代理
})

步骤 2:拦截读取操作(get)

当访问 draft.user.name 时:

  1. 首先检查 modified 对象中是否有 user 属性
  2. 如果没有,检查 obj.user 是否是对象
  3. 如果是对象,为 obj.user 创建一个新的 Proxy
  4. 这样就实现了深层嵌套对象的代理
javascript
draft.user.name
//
get(obj, 'user', receiver)
// → obj.user 是对象,返回 new Proxy(obj.user, handler)
//
get(obj.user, 'name', receiver)
// → 返回 '张三'

步骤 3:拦截写入操作(set)

当执行 draft.user.name = '李四' 时:

  1. Proxy 的 set 方法被触发
  2. 修改被记录到 modified 对象中
  3. 原始的 obj 不会被修改
javascript
draft.user.name = '李四'
//
set(obj.user, 'name', '李四')
// → modified.name = '李四'

步骤 4:生成新状态

在用户的修改函数执行完毕后:

  1. 检查 modified 对象是否为空
  2. 如果为空,说明没有任何修改,直接返回原对象(结构共享)
  3. 如果有修改,将 proxy 序列化后反序列化,生成一个新对象

关键特性

1. 结构共享(Structural Sharing)

如果没有进行任何修改,Immer 会返回原始对象,而不是创建新对象:

javascript
const obj1 = { name: '张三' }
const obj2 = produce(obj1, (draft) => {
// 没有修改
})
console.log(obj1 === obj2) // true

2. 深层嵌套支持

通过递归创建 Proxy,Immer 可以处理任意深度的嵌套对象:

javascript
produce(state, (draft) => {
draft.level1.level2.level3.value = 'new value'
})

3. 写时复制(Copy-on-Write)

只有在真正修改时才会创建新对象,未修改的部分保持原引用。

实际应用场景

在 Zustand 中使用 Immer

typescript
import { create } from 'zustand/react'
import { immer } from 'zustand/middleware/immer'
interface Store {
user: { name: string; age: number }
updateName: (name: string) => void
}
const useStore = create<Store>()(
immer((set) => ({
user: { name: '张三', age: 18 },
updateName: (name) =>
set((state) => {
// 可以直接修改 state,Immer 会处理不可变性
state.user.name = name
}),
}))
)

在 React 状态管理中

javascript
const [state, setState] = useState({ user: { name: '张三' } })
// 使用 Immer 更新深层状态
setState(produce((draft) => {
draft.user.name = '李四'
}))

实际 Immer 库的优化

上面的简化实现只是为了演示原理,实际的 Immer 库还包括:

  1. 更高效的对象合并:不使用 JSON 序列化,而是智能地合并对象
  2. 支持数组操作:可以使用 pushsplice 等数组方法
  3. 支持 Map 和 Set:扩展了对 ES6 集合类型的支持
  4. 自动冻结:在开发环境中冻结生成的对象,防止意外修改
  5. 性能优化:使用多种技术优化性能,包括对象池、懒初始化等

优势与限制

优势

更直观的代码:可以像修改可变数据一样编写代码
自动不可变性:无需手动使用扩展运算符或 Object.assign
减少样板代码:特别是在处理深层嵌套数据时
结构共享:未修改的部分保持引用相等

限制

⚠️ 必须在 produce 函数内修改:在外部修改无效
⚠️ 不支持返回值:不能在 draft 函数中 return 新值(需要特殊处理)
⚠️ 性能开销:Proxy 有一定的性能开销,但通常可以忽略不计

总结

Immer 通过巧妙地使用 JavaScript Proxy 特性,实现了以可变方式编写代码、生成不可变数据的目标。其核心原理是:

  1. 使用 Proxy 拦截对象的读写操作
  2. 记录所有的修改操作
  3. 在修改完成后,生成一个新的不可变对象
  4. 通过结构共享优化性能

这使得在 React、Redux、Zustand 等需要不可变数据的场景中,可以更方便地管理状态。

后记

My avatar

感谢你读到这里。

这座小站更像一份持续维护的“终端笔记”:记录我解决问题的过程,也记录走过的弯路。

如果这篇内容对你有一点点帮助:

  • 点个赞 / 收藏一下,方便你下次回来继续翻
  • 欢迎在评论区补充你的做法(或者指出我的疏漏)
  • 想持续收到更新:可以订阅 RSS(在页面底部)

我们下篇见。


More Posts

评论

评论 (0)

请先登录后再发表评论

加载中...