Skip to content

指南 · 基础

版本基线 Immer 11.x。本篇把「会用 produce」升到「懂机制」:draft/Proxy 工作原理、结构共享、auto-freeze、返回值规则、Map/Set 与类的支持。

一、produce 内部到底发生了什么

produce(base, recipe) 时,Immer 大致经历三步:

  1. 建代理:把 base 包成一个 Proxy(draft)。你读到的嵌套对象也会被按需包成 draft(惰性,碰到才包)。
  2. 跑配方:你在 draft 上的每次写入都被 Proxy 的 trap 拦截并记录「此节点已改」。
  3. 终态化(finalize):配方结束后,Immer 沿着「被改动的节点」复制出新对象,未改动的子树直接复用旧引用,最后(默认)递归冻结结果返回。

关键:Immer 不会一上来就深拷贝。没碰的节点不包代理、不复制;这就是它既轻量又快的原因。

二、结构共享:为什么「未变即引用不变」

Immer 只为改动路径上的节点造新副本,其余共享。于是变更检测可以靠引用比较完成:

js
const base = { a: { x: 1 }, b: { y: 2 } }
const next = produce(base, draft => {
  draft.a.x = 100 // 只动了 a
})

next !== base        // true:根变了
next.a !== base.a    // true:a 改了,新引用
next.b === base.b    // true:b 没动,共享同一引用!

next.b === base.b 为真,意味着 React memo、Redux reselect 等能据此跳过未变部分的重渲染/重算。这正是不可变数据的核心收益:便宜的变更检测 + 便宜的克隆(未变部分共享内存)。

三、auto-freeze:默认深冻结

Immer 默认开启 auto-freeze:produce 产出的状态树(含新加入结果的普通对象/数组)会被递归深冻结

js
const next = produce({ a: { x: 1 } }, draft => { draft.a.x = 2 })

next.a.x = 999 // 严格模式下抛错:Cannot assign to read only property
  • 目的:从根上防止意外 mutate 破坏不可变性。
  • 范围:递归冻结(深层也冻),但默认不冻非可枚举/非自有/symbol 属性(除非它们在配方里被 draft 过)。
  • 副作用提醒:任何进入产出结果的普通对象/数组都会被冻结,哪怕它原本没冻——所以配方并非完全无副作用。
  • 关闭:setAutoFreeze(false)(性能调优见专家篇)。

四、返回值规则(务必记牢)

写法结果
改 draft,不 return✅ 产出改动后的新状态(最常用)
改 draft,return draft✅ 同上(return draft 等价于不 return)
不改 draft,return 新对象✅ 用新对象整体替换状态
draft = 新对象❌ 无效!只是重指局部变量,改动丢失
改 draft return 新对象❌ 抛错:意图冲突
return undefined⚠️ 被当作「没替换」,不是把状态变 undefined

要把状态产出为 undefined,用哨兵值 nothing

js
import { produce, nothing } from "immer"
produce(state, draft => nothing) // 产出 undefined(而非「未改动」)

五、Map 与 Set:需先 enableMapSet()

自 v6 起,Map/Set 支持是可选插件,必须在应用启动时显式启用:

js
import { enableMapSet, produce } from "immer"
enableMapSet() // 入口处调用一次

const next = produce(new Map([["a", 1]]), draft => {
  draft.set("b", 2)
  draft.delete("a")
})
  • 不启用就把 Map/Set 放进状态并 mutate 会报错
  • Immer 产出的 Map/Set 在配方外是「人为不可变」的,对其调用 set/clear 等会抛「已冻结」错误。
  • Map 的 key 永不被 draft 化:保证 key 始终引用相等,避免语义混乱(只有 value 按需被代理)。

六、类(class):需标记 [immerable]

普通对象/数组默认可 draft,但自定义类实例默认不可,要给它打上 [immerable] = true

js
import { immerable, produce } from "immer"

class Clock {
  [immerable] = true // 标记为可 draft
  constructor(h, m) { this.hour = h; this.minute = m }
  tick() {
    return produce(this, draft => { draft.minute++ })
  }
}

const c1 = new Clock(12, 10)
const c2 = c1.tick()
c2 instanceof Clock // true:draft 保留原型

要点:draft 带上原型、只复制自有属性构造函数不会被调用;只有「成对的 getter/setter」在 draft 中可写。Date/DOM Node/Buffer 等奇异对象不支持——Date 应「创建新实例替换」而非原地改。


进入 指南 · 进阶:柯里化 producer、current/original、patches(undo/redo)、createDraft/finishDraft、与 React/Redux Toolkit 集成。