ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

Immer 入门指南:用可变的 Draft 轻松创建不可变状态 Immer 入门指南用可变的 Draft 轻松创建不可变状态【免费下载链接】immerCreate the next immutable state by mutating the current one项目地址: https://gitcode.com/gh_mirrors/im/immerImmer德语 always是一个小型 JavaScript 库其核心主张是通过修改当前状态来创建下一个不可变状态你依然用熟悉的、可变的 JavaScript API 写代码但得到的是全新的、结构共享的不可变状态。本文以 Immer 官方入门文档为主线结合本仓库源码src/core/immerClass.ts、src/core/proxy.ts 等讲解produce、draft与底层 copy-on-write写时复制原理读完你可以立刻在 React state、Redux reducer 或任意需要不可变数据的场景中落地使用。为什么需要不可变状态Immer 可以在任何需要使用不可变数据结构的上下文中使用例如 React state、React 或 Redux 的 reducer以及配置管理configuration management。不可变数据结构的价值体现在两点高效的变化检测如果对对象的引用没有改变那么对象本身也没有改变因此可以直接通过比较引用来判断状态是否变化无需深比较。廉价的克隆数据树中未更改的部分不需要复制新状态与旧版本在内存中共享这些未变化的分支——这就是结构共享structural sharing。要让这些好处成立一般做法是确保你永远不修改对象、数组或 Map 的任何属性而是始终创建一个修改后的副本。但在实践中这种约束极难手工坚持很容易被意外违反。手动不可变编码的三个痛点Immer 通过解决以下痛点来帮你遵循不可变数据范式意外变更检测Immer 会检测到意外的 mutation例如在 recipe 外部修改冻结后的状态或defineProperty/setPrototypeOf等危险操作并抛出错误。对应实现可参考 src/core/proxy.ts 中直接die(11)、die(12)的defineProperty与setPrototypeOf陷阱。消灭样板代码没有 Immer 时深度更新需要在每一层手动复制对象通常靠大量...展开操作使用 Immer 时你只对draft对象做修改Immer 会记录这些更改并负责创建必要的副本原始对象完全不受影响。无需学习专用 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参数你可以对它直接应用 mutationrecipe 执行完毕后这些 mutation 被记录并用于产生下一个状态。produce负责所有必要的复制并通过冻结数据防止未来的意外修改import {produce} from immer const nextState produce(baseState, draft { draft[1].done true draft.push({title: Tweet about it}) })注意这里可以放心使用push这种可变方法——它只作用于 draft不会触碰baseState。nextState[0]依然与baseState[0]是同一个引用结构共享而nextState[1]则是新副本。正在寻找结合 React 的 Immer可以直接跳转到 React Immer 页面 查看在useState/setState中的组合用法。Immer 如何工作Draft 是状态的代理基本思想是使用 Immer 时你将所有更改应用到一个临时的draft它是currentState的代理Proxy。一旦完成所有 mutationImmer 将根据对 draft state 的修改生成 nextState。这意味着你可以通过简单地修改数据来与数据交互同时保留不可变数据的所有好处。用一句话概括使用 Immer 就像拥有一个私人助理。助手拿一封信当前状态并给你一份副本草稿记录更改完成后助手接受你的草稿为你生成真正不可变的最终信下一个状态。从源码看 copy-on-write 的关键链路这一设计在源码里体现为一条清晰的调用链可以用 src/core/immerClass.ts 中produce的实现来印证进入作用域produce被调用时通过enterScope(this)创建一个 ImmerScope代表一次produce调用见 src/core/scope.ts随后createProxy(scope, base, undefined)为根状态创建代理 draft。执行 reciperesult recipe(proxy)在try/finally中执行——出错时revokeScope(scope)撤销所有 draft正常时leaveScope(scope)finally而非catch rethrow是为了保留原始调用栈。处理结果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 的会递归创建子 draftcreateProxy实现惰性深代理——只有被访问到的分支才会被代理这正是性能优势的来源之一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调用对应一个 ImmerScopesrc/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.tsMap/Set 还会覆盖set/add/clear/delete方法使修改直接报错。这让你在开发期就能立刻发现在 recipe 外偷偷改状态的代码。若确有性能顾虑可通过setAutoFreeze(false)关闭API 定义见 src/immer.ts。可选的插件能力Immer 采用按需加载的插件架构src/plugins/patches.ts、src/plugins/mapset.ts 与 src/plugins/arrayMethods.tsenablePatches()启用 JSON Patch 风格补丁支持produceWithPatches返回[nextState, patches, inversePatches]applyPatches可回放补丁生成逻辑见 src/plugins/patches.tsenableMapSet()让 Map 与 Set 也能被 draftsrc/plugins/mapset.tsenableArrayMethods()为数组的sort/reverse等重排方法提供优化拦截。这些插件与setUseStrictShallowCopy、setUseStrictIteration等配置一起构成了 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),仅供参考
返回列表