Immer API 全景指南:produce 与全部导出 API 的用法、原理与源码剖析
【免费下载链接】immerCreate the next immutable state by mutating the current one项目地址: https://gitcode.com/gh_mirrors/im/immer
Immer 的核心理念是"通过修改当前状态来创建下一个不可变状态"。本文以官方 API overview 文档为主线,逐一解析 Immer 暴露的全部 21 个 API:从最常用的produce,到类型工具Draft<T>/Immutable<T>、补丁系统applyPatches/produceWithPatches、手动 draft 生命周期createDraft/finishDraft,以及三个按需加载插件enablePatches()/enableMapSet()/enableArrayMethods()。读完本文,你将掌握每个 API 的签名、使用场景、底层实现原理,以及如何用Immer构造函数创建独立配置的实例。文中的源码分析均指向本仓库实际文件,可对照阅读。
API 总览
Immer 从包入口 src/immer.ts 导出一系列 API,官方文档用一张总览表统摄全局,整理如下:
| 导出名称 | 作用 | 关联章节 |
|---|---|---|
produce | Immer 的核心 API:import {produce} from "immer" | Produce |
applyPatches | 给定 base state 或 draft 和一组 patches,应用这些补丁 | Patches |
castDraft | 把不可变类型转换为对应的可变(draft)类型。仅是一次类型转换,运行时不产生任何效果 | TypeScript |
castImmutable | 把可变类型转换为对应的不可变类型。同样仅是一次类型转换 | TypeScript |
createDraft | 给定 base state,创建一个可变的 draft,对其的任何修改都会被记录 | Async |
current | 给定一个 draft 对象(不必是树根),对 draft 的当前状态拍快照 | Current |
Draft<T> | 导出的 TypeScript 类型,把不可变类型转换为可变类型 | TypeScript |
enableArrayMethods() | 启用针对数组的优化方法处理,提升数组密集型操作性能 | Array Methods |
enableMapSet() | 启用对Map与Set集合的支持 | Installation |
enablePatches() | 启用对 JSON patches 的支持 | Installation |
finishDraft | 给定由createDraft创建的 draft,封存 draft 并产出捕获了所有变更的不可变状态 | Async |
freeze(obj, deep?) | 冻结可 draft 的对象,返回原对象。默认浅冻结,第二个参数为true时递归冻结 | — |
Immer | 构造函数,可创建第二个"immer 实例",暴露本表全部 API,且不与全局实例共享配置 | — |
immerable | 可添加到构造函数或原型上的 Symbol,告诉 Immer 该类可被安全地 draft | Classes |
Immutable<T> | 导出的 TypeScript 类型,把可变类型转换为不可变类型 | — |
isDraft | 判断给定对象是否为 draft 对象 | — |
isDraftable | 判断 Immer 能否把给定对象转换为 draft。对以下对象返回true:数组、无原型的对象、以Object为原型的对象、在构造函数或原型上带有immerableSymbol 的对象 | — |
nothing | 可从 recipe 中返回的哨兵值,表示应产出undefined | Return |
original | 给定一个 draft 对象(不必是树根),返回原状态树中同一路径下的原始对象(若存在) | Original |
Patch | 导出的 TypeScript 类型,描述(反向)补丁对象的形状 | Patches |
produceWithPatches | 与produce行为一致,但额外返回补丁:结果为[result, patches, inversePatches]三元组 | Patches |
setAutoFreeze | 启用/禁用对产出树的自动冻结,默认启用 | Freezing |
setUseStrictShallowCopy | 启用严格浅拷贝。启用后,Immer 会尽可能拷贝不可枚举属性 | Classes |
setUseStrictIteration | 控制迭代行为:传false为宽松迭代(仅可枚举属性,性能更好),传true为严格迭代(包含 symbol 与不可枚举属性)。默认关闭(宽松迭代) | — |
说明:官方文档表格中
enablePatches()的链接写作./installation(缺.mdx后缀),本文统一为实际存在的 installation.mdx 路径。
导入 Immer
绝大多数场景下,你只需从 Immer 导入produce:
import {produce} from "immer"需要注意:在旧版本中produce同时以默认导出形式存在(例如import produce from "immer"也是合法的),但为了提升生态兼容性,现在已不再支持默认导出。这正是 src/immer.ts 中export const produce: IProduce = immer.produce这一命名导出的原因——所有 API 均以具名导出的方式暴露。
从 src/immer.ts 可以看到完整的导出清单:运行时 API(produce、produceWithPatches、setAutoFreeze、setUseStrictShallowCopy、setUseStrictIteration、applyPatches、createDraft、finishDraft、original、current、isDraft、isDraftable、freeze)、哨兵常量(nothing、immerable)、构造函数Immer,以及三个插件启用函数(enablePatches、enableMapSet、enableArrayMethods)与一组 TypeScript 类型(Draft、WritableDraft、Immutable、Patch、PatchListener、Producer、Objectish、StrictMode)。
produce:Immer 的核心
produce接受一个值与一个 recipe 函数(其返回值通常依赖 base state)。recipe 函数可以随意修改它的第一个参数(draft),所有修改只会作用于 base state 的拷贝之上。只有普通对象和数组会被转换为可变的 draft,其他对象一律视为不可拷贝。
基本用法
import {produce} from "immer" const baseState = [ {title: "Learn TypeScript", done: true}, {title: "Try Immer", done: false} ] const nextState = produce(baseState, draft => { draft[1].done = true draft.push({title: "Tweet about it"}) })柯里化用法
只传入一个函数,即可创建"柯里化 producer",省去每次传递 recipe 的麻烦:
const toggleTodo = produce((draft, id) => { const todo = draft.find(t => t.id === id) todo.done = !todo.done }) const nextState = toggleTodo(baseState, "todo-1")底层实现
从源码看,Immer 类的 produce 方法 的执行流程如下:
- 柯里化分支:若第一个参数是函数且第二个不是函数,则把 recipe 与默认 base 包装成一个
curriedProduce闭包返回; - 参数校验:recipe 必须是函数,否则
die(6)报错;patchListener 若存在也必须是函数; - 可 draft 分支:当
isDraftable(base)为真时,进入enterScope→createProxy创建代理 → 在try/finally中执行 recipe(出错则revokeScope,成功则leaveScope),随后用usePatchesInScope注册 patchListener,最后processResult产出最终状态。finally而非catch + rethrow的设计是为了保留原始调用栈; - 不可 draft 分支:对原始值直接调用 recipe,
undefined返回值会被替换为 base,NOTHING会被转为undefined,并按需freeze结果; - 其他情况(如 base 是函数但 recipe 也是函数)直接报错
die(1, base)。
从这一实现可以推断:produce是绑定在其Immer实例上的函数(源码注释也明确标注 "This function isboundto itsImmerinstance"),因此 src/immer.ts 中produce直接引用immer.produce即可共享全局实例的配置。
手动 draft 生命周期:createDraft 与 finishDraft
createDraft与finishDraft把"创建 draft"和"产出结果"拆成两个独立步骤,适合需要在多个异步步骤中持续修改同一份状态的场景(详见 Async)。
import {createDraft, finishDraft} from "immer" const draft = createDraft(baseState) // 在任意多个异步步骤中修改 draft await step1(draft) await step2(draft) const finalState = finishDraft(draft)源码要点
createDraft 的实现 与produce共享同一套代理创建逻辑,差异在于:
- 要求 base 必须可 draft(否则
die(8)); - 若传入的 base 本身已是 draft,会先
current(base)取快照再创建新 draft; - 创建代理后将
state.isManual_ = true标记为手动模式,并在不执行 recipe的情况下直接返回 proxy。
finishDraft 的实现 则要求 draft 存在且isManual_为真(否则die(9)),随后与produce走同样的processResult流程。其第二个参数是可选的 patchListener,传入后可根据修改生成 patches。注意:finishDraft之后不得再修改该 draft。
补丁系统:applyPatches 与 produceWithPatches
produceWithPatches:产出补丁
produceWithPatches与produce行为一致,但返回[result, patches, inversePatches]三元组:
import {produceWithPatches} from "immer" const [nextState, patches, inversePatches] = produceWithPatches( baseState, draft => { draft.x = 2 } )从 源码实现 可以看到,它内部就是调用this.produce(base, recipe, listener),并把 listener 收集到的patches与inversePatches连同结果一起打包成元组返回;它也支持柯里化调用。
applyPatches:应用补丁
applyPatches给定 base state 或 draft 与一组补丁,将补丁逐一应用(实现见 src/core/immerClass.ts#L205-L231):
import {applyPatches} from "immer" const nextState = applyPatches(baseState, [ {op: "replace", path: ["x"], value: 3} ])源码揭示了几个关键行为:
- 若存在
op: "replace"且path.length === 0的补丁(即替换整棵状态树),会直接以补丁中的 value 作为新 base,再从该补丁之后继续应用; - 若 base 是 draft,直接在 draft 上应用;否则内部走
this.produce(base, ...),即 applyPatches 本身也是一个 producer,写时复制同样生效; - 应用过程位于 patches.ts 的 applyPatches_:对每个补丁沿 path 向下解析,遇到
__proto__、constructor等危险属性会直接报错(防止原型污染),Set 不允许replace操作,数组ADD支持"-"表示 push。补丁值在应用前会做深拷贝,避免修改原补丁对象(见 deepClonePatchValue)。
Patch 类型
types-external.ts 中 Patch 接口 定义了补丁对象的形状:
export interface Patch { op: "replace" | "remove" | "add" path: (string | number)[] value?: any }补丁生成逻辑在 patches.ts:对象与 Map 走generatePatchesFromAssigned(依据assigned_记录判定 REPLACE/ADD/REMOVE),数组走generateArrayPatches(比较 base 与 copy 的长度与索引),Set 走generateSetPatches(只支持 ADD/REMOVE)。
运行时工具函数:current、original、isDraft、isDraftable、freeze
current:对 draft 拍快照
current(draft)对 draft 的当前状态拍快照并"终结化"(但不冻结),非常适合调试时打印当前状态,或把结果安全地泄露到 producer 之外(详见 Current)。它不要求 draft 是树根,任意子 draft 均可。
实现见 src/core/current.ts:
- 非 draft 参数直接报错
die(10, value); - 未修改的节点直接返回
state.base_; - 已修改的节点做浅拷贝后递归处理子节点;若未修改则直接返回原始对象,保证结构共享。
original:取原始对象
original(draft)返回原状态树中与 draft 同一路径下的原始对象(若存在),对非 draft 调用会报错die(15, value)。实现非常直接——src/utils/common.ts#L69-L73 中返回value[DRAFT_STATE].base_。注意:读到的原始对象是未修改的,因此它不可变、也不受 draft 后续修改影响。
isDraft 与 isDraftable
isDraft(value):判断是否为 Immer draft。实现 即检查对象上是否存在DRAFT_STATE标记(Symbol.for("immer-state"),定义于 src/utils/env.ts);isDraftable(value):判断能否被 draft。实现 检查四类情况:普通对象(isPlainObject)、数组、带有DRAFTABLESymbol(Symbol.for("immer-draftable"))的对象、以及 Map/Set。
其中isPlainObject的判定(src/utils/common.ts#L48-L65)相当精细:原型为null或Object.prototype的对象直接通过;否则比较构造函数的toString结果是否等于Object的,并带 WeakMap 缓存。这就是为什么"无原型的对象"和"以 Object 为原型的对象"都会被判定为可 draft。
freeze:冻结 draftable 对象
freeze(obj, deep?)冻结可 draft 对象并返回原对象。默认浅冻结;第二个参数为true时递归冻结。实现见 src/utils/common.ts#L253-L276:对 Map/Set 还会用dontMutateMethodOverride覆盖set/add/clear/delete方法,使冻结集合在尝试修改时报出"不可修改冻结集合"的错误。递归冻结时只遍历可枚举的字符串键属性(对应 issue #590 的约束,避免进入不可枚举/Symbol 属性)。
哨兵值:nothing 与 immerable
nothing:产出自定义 undefined
nothing是从 recipe 返回的哨兵值,表示"应产出undefined"(详见 Return)。其定义为Symbol.for("immer-nothing")(src/utils/env.ts#L6),在 src/immer.ts 中以NOTHING as nothing形式导出。
使用场景:当你想要把状态改为undefined而非"保持原值"时。由于 recipe 返回undefined会被视为"未修改"(返回 base),只有显式返回nothing才能表达删除意图:
import {produce, nothing} from "immer" const state = {name: "immer", version: "10"} const next = produce(state, draft => { return nothing // 产出 {name: "immer", version: "10", ...} 的 undefined 版本 })在 produce 实现 中,result === NOTHING会被转换为undefined。
immerable:让类可被 draft
immerable是Symbol.for("immer-draftable")(src/utils/env.ts#L16),添加到构造函数或原型上即可标记该类可被安全 draft(详见 Classes):
import {immerable, produce} from "immer" class Foo { [immerable] = true // 方式一:实例属性 constructor() { this.x = 1 } } // 或方式二:在类体中使用 static class Bar { static [immerable] = true }从 isDraftable 实现 可见,检查顺序是先看值本身,再看其构造函数的DRAFTABLE属性。对于"部分类(如 Date、Promise)"而言,没有这个标记就不会被 draft,从而保持其原生行为。
配置 API:setAutoFreeze、setUseStrictShallowCopy、setUseStrictIteration
这三个 API 均绑定在全局 immer 实例上(src/immer.ts#L62-L79),对应 Immer 类的内部字段:
| 配置项 | 默认值 | 作用 |
|---|---|---|
setAutoFreeze(value) | true | 自动冻结所有由 Immer 创建的拷贝(详见 Freezing)。源码注释强调:"始终默认冻结,即使在生产模式也一样" |
setUseStrictShallowCopy(value) | false | 启用严格浅拷贝。默认情况下 Immer不拷贝 getter/setter 等属性描述符与不可枚举属性;启用后尽可能拷贝(src/utils/common.ts#L200-L244 的shallowCopy中对应strict === true分支会用Object.getOwnPropertyDescriptors+Object.create重建对象) |
setUseStrictIteration(value) | false | 控制迭代行为:false为宽松迭代(仅可枚举字符串属性,性能更好),true为严格迭代(包含 symbol 与不可枚举属性)。注意setUseStrictShallowCopy支持boolean \| "class_only"三种取值(StrictMode类型,见 src/core/immerClass.ts#L45),而setUseStrictIteration仅接受布尔值 |
import {setAutoFreeze} from "immer" setAutoFreeze(false) // 关闭自动冻结,提升性能;需要自己保证状态不被意外修改严格迭代/严格拷贝模式对应 src/utils/common.ts 的 each 函数:严格模式下用Reflect.ownKeys(obj)遍历全部自有键,宽松模式用Object.keys(obj)仅遍历可枚举字符串键。
按需加载的插件:enablePatches、enableMapSet、enableArrayMethods
Immer 的核心包默认只支持普通对象与数组。Map/Set、JSON 补丁、数组方法优化这三项能力以插件形式按需启用,调用对应的enableXxx()后立即生效。插件注册机制在 src/utils/plugins.ts:loadPlugin只加载一次,getPlugin在插件未加载时抛出错误(die(0, pluginKey)),插件键分别为"MapSet"、"Patches"、"ArrayMethods"。
enablePatches()
启用 JSON patches 支持,即produceWithPatches、applyPatches与produce的 patchListener 参数的前提条件。实现位于 src/plugins/patches.ts,注册了四个能力:applyPatches_(应用补丁)、generatePatches_(按对象/数组/Set 分派生成补丁)、generateReplacementPatches_(整树替换补丁)、getPath(从 draft 状态回溯补丁路径)。
import {enablePatches, produceWithPatches} from "immer" enablePatches() const [nextState, patches, inversePatches] = produceWithPatches( base, draft => { draft.x = 2 } )enableMapSet()
启用Map与Set支持(详见 Map-Set)。实现位于 src/plugins/mapset.ts:内部定义DraftMap extends Map与DraftSet extends Set两个子类,通过覆写get/set/delete/add/clear等方法来记录修改(assigned_映射)并延迟创建拷贝(prepareMapCopy+markChanged)。值得注意的实现细节(src/plugins/mapset.ts#L23-L32):通过globalThis.Iterator.from做特性检测(ES2025 新增),目的是在不污染全局声明的前提下获得迭代器能力。
import {enableMapSet, produce} from "immer" enableMapSet() const next = produce(new Map([["a", 1]]), draft => { draft.set("b", 2) })enableArrayMethods()
启用数组方法的优化处理,对数组密集型操作有显著性能提升(详见 Array Methods)。实现位于 src/plugins/arrayMethods.ts,其核心思想是避免在迭代过程中为每个元素创建 Proxy:
- 变更类方法(
push、pop、shift、unshift、splice、reverse、sort):直接操作拷贝,不创建逐元素代理; - 子集操作(
filter、slice、find、findLast):返回 draft 代理,后续修改仍会被追踪; - 变换操作(
concat、flat):返回 base 值,修改不会被追踪; - 返回原始值的方法(
findIndex、indexOf、includes、some、every、join等):无需 draft。
重要注意事项:被覆写方法的回调接收的是base 值而非 draft——这正是性能优化的核心,但也意味着在回调中修改传入的元素不会影响最终状态(arrayMethods.ts#L79-L96 的注释与示例明确说明了这一点)。
import {enableArrayMethods, produce} from "immer" enableArrayMethods() const next = produce(state, draft => { // 优化路径:不创建逐元素代理 draft.items.sort((a, b) => a.value - b.value) // filter 返回 drafts,修改可传播 const filtered = draft.items.filter(x => x.value > 5) filtered[0].value = 999 // 会影响 draft.items 中的对应元素 })Immer 构造函数:创建独立配置的实例
Immer是构造函数,可创建第二个"immer 实例",它暴露本表列出的全部 API,但不共享全局实例的配置。这在需要"一个应用里同时存在不同冻结策略"的场景下非常有用。
import {Immer} from "immer" const immer = new Immer({autoFreeze: false}) const next = immer.produce(base, draft => { draft.x = 1 })构造函数签名 接受一个可选配置对象:
new Immer({ autoFreeze?: boolean useStrictShallowCopy?: boolean | "class_only" useStrictIteration?: boolean })传入的配置会立即调用对应的 setter 方法。从源码结构可以推断,全局导出的produce等函数就是new Immer()默认实例的方法绑定(src/immer.ts#L27-L48 中const immer = new Immer()之后才有export const produce = immer.produce),因此自定义实例与全局实例的配置互不影响。
TypeScript 类型工具:Draft、Immutable、castDraft、castImmutable、Patch
Immer 导出一组类型工具,帮助在类型层面打通"不可变 ↔ 可变"的转换(详见 TypeScript)。
Draft 与 Immutable
Draft<T>把不可变类型转换为可变类型;Immutable<T>反向操作。两者的定义位于 src/types/types-external.ts#L59-L86,遵循相同的递归结构:
- 原始类型与"原子对象"(
AtomicObject,如Date、Promise)原样保留; ReadonlyMap<K, V>→Map<Draft<K>, Draft<V>>(反向为ReadonlyMap<Immutable<K>, Immutable<V>>);ReadonlySet<V>→Set<Draft<V>>;- 普通对象:
Draft通过WritableDraft去掉readonly并递归,Immutable则给每个键加上readonly并递归。
另外还导出WritableDraft<T>(仅去readonly、不做类型转换,src/types/types-external.ts#L31-L37),以及对元组类型做了专门处理(IsPlainArray判别普通数组与元组,避免元组被拓宽为Element[])。
castDraft 与 castImmutable
这两个函数运行时是 no-op,纯粹用于类型断言,让 TypeScript 在无法自动推导时"闭嘴"(src/immer.ts#L110-L117):
export let castDraft = <T>(value: T): Draft<T> => value as any export let castImmutable = <T>(value: T): Immutable<T> => value as any典型用法是在 Redux 等外部代码传入宽泛类型时使用(参考测试 redux.ts 与 type-external.ts)。
Patch 与 PatchListener
Patch描述补丁对象形状(op: "replace" | "remove" | "add"、path: (string | number)[]、可选value);PatchListener是补丁监听器类型(patches: Patch[], inversePatches: Patch[]) => void(src/types/types-external.ts#L88-L94),即produce第三参数与finishDraft第二参数的类型。
常见问题与最佳实践
- 必须使用命名导入:
import {produce} from "immer",旧式的默认导入已不再支持; current与original的区别:current返回 draft 的当前快照(包含未提交的修改),original返回未被修改的原始对象。调试打印用current,读取初始值用original;- 插件必须在调用相关 API 前启用:未调用
enablePatches()就使用produceWithPatches会因插件未加载而报错(getPlugin的die(0, pluginKey));Map/Set同理依赖enableMapSet(); enableArrayMethods()的回调陷阱:回调中收到的是 base 值,不要在回调内部直接修改传入元素并期望它反映到结果中;想修改请对filter/slice等返回的 draft 结果进行操作;- 自动冻结与性能:
setAutoFreeze(false)可以换取性能,但会失去"修改产出状态即报错"的保护(StrictMode相关检查可见 src/utils/env.ts 与 src/core/finalize.ts),需自行保证状态不可变性; - 需要多实例配置时用
new Immer(config):全局 setter 影响所有使用命名导出函数的代码,独立实例则隔离配置。
延伸阅读
- produce 完整指南:柯里化、返回值约定与 recipe 细节
- 补丁系统:
produceWithPatches与applyPatches的完整示例 - 异步与手动 draft:
createDraft/finishDraft的异步场景 - Map/Set 支持:
enableMapSet的用法与限制 - 数组方法优化:
enableArrayMethods的性能细节 - TypeScript 指南:
Draft/Immutable/castDraft/castImmutable的类型技巧 - Freezing 与 Classes:冻结策略与自定义类支持
- 源码入口:src/immer.ts(导出清单)、src/core/immerClass.ts(核心实现)、src/plugins/(三个插件)、src/utils/common.ts(工具函数)、src/types/types-external.ts(公开类型)
【免费下载链接】immerCreate the next immutable state by mutating the current one项目地址: https://gitcode.com/gh_mirrors/im/immer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考