Immer 入门指南:用可变的 Draft 轻松创建不可变状态
2026/9/19 6:47:37 网站建设 项目流程

Immer 入门指南:用可变的 Draft 轻松创建不可变状态

【免费下载链接】immerCreate the next immutable state by mutating the current one项目地址: https://gitcode.com/gh_mirrors/im/immer

Immer(德语 "always")是一个小型 JavaScript 库,其核心主张是"通过修改当前状态来创建下一个不可变状态":你依然用熟悉的、可变的 JavaScript API 写代码,但得到的是全新的、结构共享的不可变状态。本文以 Immer 官方入门文档为主线,结合本仓库源码(src/core/immerClass.ts、src/core/proxy.ts 等)讲解producedraft与底层 copy-on-write(写时复制)原理,读完你可以立刻在 React state、Redux reducer 或任意需要不可变数据的场景中落地使用。

为什么需要不可变状态

Immer 可以在任何需要使用不可变数据结构的上下文中使用,例如 React state、React 或 Redux 的 reducer,以及配置管理(configuration management)。不可变数据结构的价值体现在两点:

  • 高效的变化检测:如果对对象的引用没有改变,那么对象本身也没有改变,因此可以直接通过===比较引用来判断状态是否变化,无需深比较。
  • 廉价的克隆:数据树中未更改的部分不需要复制,新状态与旧版本在内存中共享这些未变化的分支——这就是"结构共享"(structural sharing)。

要让这些好处成立,一般做法是:确保你永远不修改对象、数组或 Map 的任何属性,而是始终创建一个"修改后的副本"。但在实践中,这种约束极难手工坚持,很容易被意外违反。

手动不可变编码的三个痛点

Immer 通过解决以下痛点来帮你遵循不可变数据范式:

  1. 意外变更检测:Immer 会检测到意外的 mutation(例如在 recipe 外部修改冻结后的状态,或defineProperty/setPrototypeOf等危险操作)并抛出错误。对应实现可参考 src/core/proxy.ts 中直接die(11)die(12)definePropertysetPrototypeOf陷阱。
  2. 消灭样板代码:没有 Immer 时,深度更新需要在每一层手动复制对象,通常靠大量...展开操作;使用 Immer 时,你只对draft对象做修改,Immer 会记录这些更改并负责创建必要的副本,原始对象完全不受影响。
  3. 无需学习专用 API:使用 Immer 不需要学习新的数据结构或 mutation 模式,你操作的依然是纯 JavaScript 对象、数组、Map 和 Set,使用的是大家熟知且安全可变的 JavaScript API。

一个简单的对比示例

假设我们有如下基础状态,需要更新第二个 todo 的完成状态,并新增第三个 todo;同时要求不改变原始的baseState,也避免深度克隆(以保留第一个 todo 的引用,实现结构共享):

const baseState = [ { title: "Learn TypeScript", done: true }, { title: "Try Immer", done: false } ]

不使用 Immer

没有 Immer 时,必须小心地浅拷贝每一层受更改影响的状态结构

const nextState = baseState.slice() // 浅拷贝数组 nextState[1] = { // 替换第一层元素 ...nextState[1], // 浅拷贝第一层元素 done: true // 期望的更新 } // 因为 nextState 是新拷贝的, 所以使用 push 方法是安全的, // 但是在未来的任意时间做相同的事情会违反不变性原则并且导致 bug! nextState.push({title: "Tweet about it"})

这段代码的正确性完全依赖开发者"每一层都不要漏掉拷贝"的自觉——随着状态树变深,漏拷一层的风险随之上升。

使用 Immer

使用 Immer,这个过程简单得多。produce函数接收两个参数:要更改的baseState,以及一个名为 recipe 的函数。recipe 接收一个draft参数,你可以对它直接应用 mutation;recipe 执行完毕后,这些 mutation 被记录并用于产生下一个状态。produce负责所有必要的复制,并通过冻结数据防止未来的意外修改:

import {produce} from "immer" const nextState = produce(baseState, draft => { draft[1].done = true draft.push({title: "Tweet about it"}) })

注意这里可以放心使用push这种可变方法——它只作用于 draft,不会触碰baseStatenextState[0]依然与baseState[0]是同一个引用(结构共享),而nextState[1]则是新副本。

正在寻找结合 React 的 Immer?可以直接跳转到 React + Immer 页面 查看在useState/setState中的组合用法。

Immer 如何工作:Draft 是状态的代理

基本思想是:使用 Immer 时,你将所有更改应用到一个临时的draft,它是currentState的代理(Proxy)。一旦完成所有 mutation,Immer 将根据对 draft state 的修改生成 nextState。这意味着你可以通过简单地修改数据来与数据交互,同时保留不可变数据的所有好处。

用一句话概括:使用 Immer 就像拥有一个私人助理。助手拿一封信(当前状态)并给你一份副本(草稿)记录更改;完成后,助手接受你的草稿,为你生成真正不可变的最终信(下一个状态)。

从源码看 copy-on-write 的关键链路

这一设计在源码里体现为一条清晰的调用链,可以用 src/core/immerClass.ts 中produce的实现来印证:

  1. 进入作用域produce被调用时,通过enterScope(this)创建一个 ImmerScope(代表一次produce调用,见 src/core/scope.ts),随后createProxy(scope, base, undefined)为根状态创建代理 draft。
  2. 执行 reciperesult = recipe(proxy)try/finally中执行——出错时revokeScope(scope)撤销所有 draft,正常时leaveScope(scope)finally而非catch + rethrow是为了保留原始调用栈。
  3. 处理结果usePatchesInScope按需启用 patch 监听,随后processResult(result, scope)完成最终化(见 src/core/finalize.ts)。

produce的入口实现(src/core/immerClass.ts)还包含两点值得注意的行为:

  • 柯里化调用:当第一个参数是函数而第二个不是时,进入curriedProduce分支,返回一个可以复用的 producer,避免每次重复传 recipe。
  • 非 draftable 值:对不满足 draftable 条件的值(如原始类型),直接执行 recipe 并按需冻结,此时不产生代理。

代理陷阱:读时创建、写时复制

draft 本质是Proxy.revocable创建的代理,其目标(target)就是内部状态对象本身,见 src/core/proxy.ts 的createProxyProxy。两个最核心的陷阱(trap)是:

  • get陷阱(读):访问 draft 属性时,如果该值是 draftable 的,会递归创建子 draft(createProxy),实现"惰性深代理"——只有被访问到的分支才会被代理,这正是性能优势的来源之一(src/core/proxy.ts)。
  • set陷阱(写):写入时若值未变(is(value, current)判断,含 NaN 特殊处理)则忽略;否则调用prepareCopy(state)浅拷贝出copy_,并通过markChanged(state)递归向上标记"已修改"(src/core/proxy.ts、src/core/proxy.ts)。只有被写入的节点才产生拷贝,未触碰的分支继续共享原引用——这就是写时复制(copy-on-write)与结构共享的直接实现。

另外,getOwnPropertyDescriptor陷阱会把所有属性描述符重写为 writable/configurable,从而保证草案可以被自由修改(src/core/proxy.ts)。

作用域与最终化

每次produce调用对应一个 ImmerScope(src/core/scope.ts),它登记了本次调用创建的所有 drafts、可选的 patch 插件与 map/set 插件。recipe 结束后,finalize递归处理整棵树(src/core/finalize.ts):

  • 未修改的 draft直接返回(冻结的)原始 base,不产生任何拷贝;
  • 已修改的 draft最终返回copy_,并通过maybeFreeze在启用自动冻结时递归冻结结果(src/core/finalize.ts);
  • 完成后再revokeScope撤销全部代理,防止 draft 逃逸后被继续修改。

isDraftable判定(src/utils/common.ts)表明:纯对象、数组、Map、Set 以及标记了immerable的类实例才可被 draft。

开箱即用的对象冻结

自动冻结(auto-freeze)默认开启:所有由 Immer 生成的副本都会被Object.freeze深冻结(实现见 src/utils/common.ts,Map/Set 还会覆盖set/add/clear/delete方法使修改直接报错)。这让你在开发期就能立刻发现"在 recipe 外偷偷改状态"的代码。若确有性能顾虑,可通过setAutoFreeze(false)关闭(API 定义见 src/immer.ts)。

可选的插件能力

Immer 采用按需加载的插件架构(src/plugins/patches.ts、src/plugins/mapset.ts 与 src/plugins/arrayMethods.ts):

  • enablePatches():启用 JSON Patch 风格补丁支持,produceWithPatches返回[nextState, patches, inversePatches]applyPatches可回放补丁,生成逻辑见 src/plugins/patches.ts;
  • enableMapSet():让 Map 与 Set 也能被 draft(src/plugins/mapset.ts);
  • enableArrayMethods():为数组的sort/reverse等重排方法提供优化拦截。

这些插件与setUseStrictShallowCopysetUseStrictIteration等配置一起,构成了 src/immer.ts 对外暴露的完整 API 面。

好处

综合来看,Immer 带来的核心收益如下(原文出处:中文入门文档):

  • 遵循不可变数据范式,同时使用普通的 JavaScript 对象、数组、Set 和 Map,无需学习新的 API 或 "mutations patterns";
  • 强类型,无基于字符串的路径选择器等;
  • 开箱即用的结构共享;
  • 开箱即用的对象冻结;
  • 深度更新轻而易举;
  • 样板代码减少:更少的噪音,更简洁的代码;
  • 对 JSON 补丁的一流支持;
  • 小体积:3KB gzip(官方文档声明)。

仓库本身也印证了"零依赖"这一事实:package.json中没有声明任何运行时dependencies,且tests/base.js 中专门有一条测试"immer should have no dependencies"来守护这一点。当前仓库package.json标注的版本为10.0.3-beta(见 package.json),源码入口为 src/immer.ts,构建与测试脚本均可通过yarn运行(package.json)。

下一步

想进一步掌握 Immer 的日常用法,建议继续阅读 produce 使用指南——它详细讲解了 recipe 的写法、深层修改、柯里化以及"返回新数据替换整个 draft"的特殊用法;如果要在 React 生态中使用,请直接查看 React + Immer;涉及补丁(patches)同步多端状态时,可参考 补丁文档。项目根目录的 readme.md 提供了项目概览,全部源码位于 src/ 目录,核心实现集中在core/(代理、作用域、最终化)与plugins/(patches、mapset、arrayMethods)子目录,测试覆盖见tests/。

【免费下载链接】immerCreate the next immutable state by mutating the current one项目地址: https://gitcode.com/gh_mirrors/im/immer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询