immer 笔记
约 2236 字大约 7 分钟
2025-12-22
Immer 不是“必需品”,而是“复杂状态管理里的效率放大器”
Redux Toolkit 内置 Immer,大量项目“在用 Immer 但自己都没意识到”
刻意不用 Immer 可能因为:非 Proxy 环境(老浏览器、特殊 runtime)
不是用 Immer 的人少,而是「显式使用 Immer 的人少」,大量项目是“间接、无感地”在用 Immer。
immer — 底层不可变状态库
这是核心库,用来实现不可变状态更新的逻辑:
- 你写看起来像可变的代码(修改 draft),
- 底层把它转成真正的不可变更新(copy-on-write、结构共享等)
它不依赖 React,可以用于任何 JavaScript/TypeScript 项目
use-immer — 为 React 提供的 Hook 封装
这个库是建立在 immer 之上的一个 React Hooks 工具库。
用于像 useState 一样管理 state,状态更新可以传入一个 immer producer 函数,自动触发 React 重渲。
不可变更新是为“快速、可靠地判断『这个对象变没变』”发明的。
早期前端 & 原生 JS 的真实问题:[“引用不变,内部变了,但我不知道”] obj.a.b.c = 1,JS 本身 没有对象变更事件,只能手写 getter / setter,
不可变对象:nextState !== prevState 框架只做一件事:比较引用,决定要不要更新,代价:更新时你要创建新对象,收益:不需要深度监听,没有依赖图
现代 Vue 是支持监听属性变化的,Vue 3:Proxy,精细到属性级别,自动依赖收集,所以在 Vue 里:state.a.b.c++ 是合法、推荐、舒适的
React 的哲学是:我不监听属性,我让你在更新时告诉我“我变了”。
但是有些问题 监听解决不了,比如判断两个状态是否“逻辑上相同”,撤销 / 回滚,快照对比,跨线程 / 跨时间安全读,与非响应式系统交互(Web Worker、缓存、持久化)
以前:监听属性变化成本极高 → 不可变 + 引用比较胜出
现在:Proxy 让监听变便宜了,但 React / Preact 的架构仍然选择“不监听”
因此:Immer 在这些生态里依然非常有价值
Immer 不是“因为监听太贵才诞生的”,而是“为了让不可变更新不痛苦”而诞生的。
关于冻结机制
Immer 是一个用于简化状态管理的库,尤其是在使用不可变数据结构时。它的核心功能是通过 produce 函数对状态进行变更,但仍然能够保持不可变性。
produce 函数
produce 函数是 Immer 的核心函数,用于对 state 进行修改。
参数:
- state:要进行修改的原始状态。
- recipe:一个函数,该函数接受
draft作为参数,允许我们对draft对象进行直接修改(mutation)。
import { produce } from 'immer';
const newState = produce(state, draft => {
// 在这里对 draft 进行修改
});执行流程:
- 在
produce中传入原始状态和一个recipe函数。 recipe会对draft(草稿状态)进行直接的修改。- 在
recipe完成后,produce会返回一个新的状态对象,该对象仅包含修改的部分。 - Immer 会负责复制修改,并通过冻结数据来防止意外修改。
深度冻结
- 默认情况下,Immer 会深度冻结(deep freeze)生成的状态对象,即新对象的每一层都被冻结,防止任何后续的修改。
- 这意味着对嵌套的对象进行修改时,Immer 会确保每一层对象都受到保护,无法被直接修改。
防止意外修改
Immer 会检测到任何意外的修改(如试图在冻结的对象上进行修改)并抛出错误。
例子:
- 一旦通过
produce创建的新对象完成,任何对这个对象的修改都会被捕获并抛出错误。 - 这能够有效地防止意外的状态修改。
Immer 真正“实用”的地方
- 按需拷贝(copy-on-write)
- 结构共享(structural sharing)
- 自动判断要不要开辟新空间(only copy what changed)
- 没改的部分复用原引用(re-use everything else)
Immer 的核心模型:Copy-on-Write + Structural Sharing
它不会一上来就全量拷贝,而是:
- 先拿一份原对象
baseState - 给你一个
draft(Proxy 包着) - 你“看起来像修改”
- Immer 在背后记录 diff
- 最后根据 diff 生成新对象
**按需拷贝 **只在第一次写的时候才拷贝
Immer 的逻辑大概是:
读取时:直接读 baseState,写入时:第一次写到某个对象时,才 shallow copy 那一层
一个最直观的例子:只改一处,只有那条路径复制
import { produce } from "immer"
const baseState = {
a: { x: 1 },
b: { y: 2 },
c: { z: 3 }
}
const nextState = produce(baseState, draft => {
draft.b.y = 999
})nextState !== baseState // true ✅ root 变了(因为至少一处变)
nextState.a === baseState.a // true ✅ a 没动,复用
nextState.b === baseState.b // false ❌ b 动了,被拷贝
nextState.c === baseState.c // true ✅ c 没动,复用这就是结构共享:只复制了 root 和 b,其余都复用。
结构共享的本质:把“变化”限制在最小路径上
比如你改的是:
root.b.y那么 Immer 复制的路径是:
root (必复制,因为 root 的引用要变)
└─ b (必复制,因为 b 的内容变了)
└─ y (primitive,直接改)其余的 a / c 依旧指向老对象。
Immer 的三个关键点
- draft 是 Proxy,不是 clone,你以为你在改对象,其实是在改一个“记录器”。
- 按需 shallow copy,不做 deep copy,拷贝发生在写入时,且只浅拷贝当前层。
- 没改的东西完全复用引用(结构共享),这就是它“便宜”的根本原因。
为什么说它比 deepClone 便宜?
deepClone 的成本:
- 无论你改哪儿:全部复制
- O(N)
Immer 的成本:
- 只复制修改路径上的节点
- O(depth of changed path + size of changed substructures)
所以你改一处的时候成本是:
O(树的高度) 而不是整个树大小。
常用 API
enableMapSet 和 disableMapSet
默认情况下,Immer 只支持普通的 JavaScript 对象和数组。如果你希望支持 Map 和 Set 类型的不可变操作,可以使用这两个 API。
enableMapSet:启用对Map和Set的支持。disableMapSet:禁用对Map和Set的支持。
import produce, { enableMapSet } from 'immer';
enableMapSet();
const state = new Map();
const nextState = produce(state, draft => {
draft.set('key', 'value');
});createDraft
createDraft 允许你创建一个草稿对象。它本质上是 produce 的一个低级 API,允许你创建并修改草稿(draft),但没有像 produce 那样立即返回修改后的新状态对象。可以在之后调用 finishDraft 来获取修改后的状态。
import { createDraft, finishDraft } from 'immer';
const draft = createDraft({ a: 1 });
draft.a = 2; // 修改草稿对象
const newState = finishDraft(draft); // 完成并返回新状态setAutoFreeze 和 getAutoFreeze
setAutoFreeze:这个 API 允许你启用或禁用 Immer 自动冻结新生成的状态对象。默认情况下,Immer 会冻结新对象,防止它们被意外修改。如果你想禁用这种行为,可以通过setAutoFreeze(false)来关闭冻结功能。getAutoFreeze:用于检查当前是否启用了自动冻结。
import { setAutoFreeze, getAutoFreeze } from 'immer';
setAutoFreeze(false); // 禁用自动冻结
console.log(getAutoFreeze()); // 输出 falseoriginal
original 是一个非常有用的 API,它可以用来获取当前草稿对象对应的原始对象。如果你需要查看当前草稿在未修改前的状态,可以使用这个方法。
import produce, { original } from 'immer';
const state = { a: 1, b: 2 };
const nextState = produce(state, draft => {
draft.a = 3;
});
console.log(original(nextState)); // 输出 { a: 1, b: 2 }Draft 类型
在 TypeScript 中,Immer 提供了 Draft 类型,帮助确保草稿对象的类型安全。Draft 类型是对原始对象的代理,它允许你在 produce 函数中直接修改草稿对象而不会影响原始状态。
import produce, { Draft } from 'immer';
type State = { a: number };
const state: State = { a: 1 };
const nextState = produce(state, (draft: Draft<State>) => {
draft.a = 2;
});相关信息
为什么 Map 和 Set 需要单独启用?
API 不同:
Map和Set的操作方法与普通对象不同。普通对象可以通过直接修改属性来改变其状态(例如,draft.key = value)。然而,Map和Set提供的 API 需要使用特定的方法进行修改:Map通过.set()方法修改键值对。Set通过.add()方法添加元素。
- 因为
Immer是基于Proxy实现的,默认情况下它支持对象和数组的直接修改(通过属性赋值),但是对于Map和Set,它需要特别处理,以便在produce中追踪它们的状态变化。
体积增加:启用对 Map 和 Set 的支持,会使 Immer 包含更多的代码逻辑,尤其是处理这些对象的变化跟踪。这种额外的支持会导致打包体积的增加,尽管这个增加相对来说是比较小的,但如果你关心包的大小,特别是在资源有限的项目中,启用这些功能可能不是必须的。