Immer API 全景指南:produce 与全部导出 API 的用法、原理与源码剖析
Immer API 全景指南produce 与全部导出 API 的用法、原理与源码剖析【免费下载链接】immerCreate the next immutable state by mutating the current one项目地址: https://gitcode.com/gh_mirrors/im/immerImmer 的核心理念是通过修改当前状态来创建下一个不可变状态。本文以官方 API overview 文档为主线逐一解析 Immer 暴露的全部 21 个 API从最常用的produce到类型工具DraftT/ImmutableT、补丁系统applyPatches/produceWithPatches、手动 draft 生命周期createDraft/finishDraft以及三个按需加载插件enablePatches()/enableMapSet()/enableArrayMethods()。读完本文你将掌握每个 API 的签名、使用场景、底层实现原理以及如何用Immer构造函数创建独立配置的实例。文中的源码分析均指向本仓库实际文件可对照阅读。API 总览Immer 从包入口 src/immer.ts 导出一系列 API官方文档用一张总览表统摄全局整理如下导出名称作用关联章节produceImmer 的核心 APIimport {produce} from immerProduceapplyPatches给定 base state 或 draft 和一组 patches应用这些补丁PatchescastDraft把不可变类型转换为对应的可变draft类型。仅是一次类型转换运行时不产生任何效果TypeScriptcastImmutable把可变类型转换为对应的不可变类型。同样仅是一次类型转换TypeScriptcreateDraft给定 base state创建一个可变的 draft对其的任何修改都会被记录Asynccurrent给定一个 draft 对象不必是树根对 draft 的当前状态拍快照CurrentDraftT导出的 TypeScript 类型把不可变类型转换为可变类型TypeScriptenableArrayMethods()启用针对数组的优化方法处理提升数组密集型操作性能Array MethodsenableMapSet()启用对Map与Set集合的支持InstallationenablePatches()启用对 JSON patches 的支持InstallationfinishDraft给定由createDraft创建的 draft封存 draft 并产出捕获了所有变更的不可变状态Asyncfreeze(obj, deep?)冻结可 draft 的对象返回原对象。默认浅冻结第二个参数为true时递归冻结—Immer构造函数可创建第二个immer 实例暴露本表全部 API且不与全局实例共享配置—immerable可添加到构造函数或原型上的 Symbol告诉 Immer 该类可被安全地 draftClassesImmutableT导出的 TypeScript 类型把可变类型转换为不可变类型—isDraft判断给定对象是否为 draft 对象—isDraftable判断 Immer 能否把给定对象转换为 draft。对以下对象返回true数组、无原型的对象、以Object为原型的对象、在构造函数或原型上带有immerableSymbol 的对象—nothing可从 recipe 中返回的哨兵值表示应产出undefinedReturnoriginal给定一个 draft 对象不必是树根返回原状态树中同一路径下的原始对象若存在OriginalPatch导出的 TypeScript 类型描述反向补丁对象的形状PatchesproduceWithPatches与produce行为一致但额外返回补丁结果为[result, patches, inversePatches]三元组PatchessetAutoFreeze启用/禁用对产出树的自动冻结默认启用FreezingsetUseStrictShallowCopy启用严格浅拷贝。启用后Immer 会尽可能拷贝不可枚举属性ClassessetUseStrictIteration控制迭代行为传false为宽松迭代仅可枚举属性性能更好传true为严格迭代包含 symbol 与不可枚举属性。默认关闭宽松迭代—说明官方文档表格中enablePatches()的链接写作./installation缺.mdx后缀本文统一为实际存在的 installation.mdx 路径。导入 Immer绝大多数场景下你只需从 Immer 导入produceimport {produce} from immer需要注意在旧版本中produce同时以默认导出形式存在例如import produce from immer也是合法的但为了提升生态兼容性现在已不再支持默认导出。这正是 src/immer.ts 中export const produce: IProduce immer.produce这一命名导出的原因——所有 API 均以具名导出的方式暴露。从 src/immer.ts 可以看到完整的导出清单运行时 APIproduce、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。produceImmer 的核心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 分支对原始值直接调用 recipeundefined返回值会被替换为 baseNOTHING会被转为undefined并按需freeze结果其他情况如 base 是函数但 recipe 也是函数直接报错die(1, base)。从这一实现可以推断produce是绑定在其Immer实例上的函数源码注释也明确标注 This function isboundto itsImmerinstance因此 src/immer.ts 中produce直接引用immer.produce即可共享全局实例的配置。手动 draft 生命周期createDraft 与 finishDraftcreateDraft与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 与 produceWithPatchesproduceWithPatches产出补丁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-L231import {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、freezecurrent对 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 与 isDraftableisDraft(value)判断是否为 Immer draft。实现 即检查对象上是否存在DRAFT_STATE标记Symbol.for(immer-state)定义于 src/utils/env.tsisDraftable(value)判断能否被 draft。实现 检查四类情况普通对象isPlainObject、数组、带有DRAFTABLESymbolSymbol.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 与 immerablenothing产出自定义 undefinednothing是从 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让类可被 draftimmerable是Symbol.for(immer-draftable)src/utils/env.ts#L16添加到构造函数或原型上即可标记该类可被安全 draft详见 Classesimport {immerable, produce} from immer class Foo { [immerable] true // 方式一实例属性 constructor() { this.x 1 } } // 或方式二在类体中使用 static class Bar { static [immerable] true }从 isDraftable 实现 可见检查顺序是先看值本身再看其构造函数的DRAFTABLE属性。对于部分类如 Date、Promise而言没有这个标记就不会被 draft从而保持其原生行为。配置 APIsetAutoFreeze、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.getOwnPropertyDescriptorsObject.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、enableArrayMethodsImmer 的核心包默认只支持普通对象与数组。Map/Set、JSON 补丁、数组方法优化这三项能力以插件形式按需启用调用对应的enableXxx()后立即生效。插件注册机制在 src/utils/plugins.tsloadPlugin只加载一次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_映射并延迟创建拷贝prepareMapCopymarkChanged。值得注意的实现细节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、PatchImmer 导出一组类型工具帮助在类型层面打通不可变 ↔ 可变的转换详见 TypeScript。Draft 与 ImmutableDraftT把不可变类型转换为可变类型ImmutableT反向操作。两者的定义位于 src/types/types-external.ts#L59-L86遵循相同的递归结构原始类型与原子对象AtomicObject如Date、Promise原样保留ReadonlyMapK, V→MapDraftK, DraftV反向为ReadonlyMapImmutableK, ImmutableVReadonlySetV→SetDraftV普通对象Draft通过WritableDraft去掉readonly并递归Immutable则给每个键加上readonly并递归。另外还导出WritableDraftT仅去readonly、不做类型转换src/types/types-external.ts#L31-L37以及对元组类型做了专门处理IsPlainArray判别普通数组与元组避免元组被拓宽为Element[]。castDraft 与 castImmutable这两个函数运行时是 no-op纯粹用于类型断言让 TypeScript 在无法自动推导时闭嘴src/immer.ts#L110-L117export let castDraft T(value: T): DraftT value as any export let castImmutable T(value: T): ImmutableT value as any典型用法是在 Redux 等外部代码传入宽泛类型时使用参考测试 redux.ts 与 type-external.ts。Patch 与 PatchListenerPatch描述补丁对象形状op: replace | remove | add、path: (string | number)[]、可选valuePatchListener是补丁监听器类型(patches: Patch[], inversePatches: Patch[]) voidsrc/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的完整示例异步与手动 draftcreateDraft/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),仅供参考