Immer.js

React 中常见的复杂数据修改方式如下,对于复杂数据通常先借用 cloneDeep 进行深拷贝,修改后再 setState 刷新视图。

interface Data {
    id: number;
    name: string;
    children: Data[];
}

const [data, setData] = useState();

const _data = cloneDeep(data);

_data.children[0].name = 'abc'
_data.children[0].children[1].id = 123;

setData(_data);
存在的问题
  1. deepClone 全量深拷贝耗时
  2. 对于未修改的数据,由于深拷贝的原因,导致数据进行了变化
  3. 对于重渲染性能的应用来说,深拷贝导致 props 浅比较不相同,组件将会重新渲染

介于上述原因,社区提供了一个适配 React 且更改复杂数据非常便捷的库 immer.js

produce

创建 scope,创建 proxy,调用 draft 的 change handler 生成最终 result,最后调用 patchListenser 以及冻结 result 并返回。

src/core/proxy.ts

immer.js 的核心,将数据 proxy 化。其核心 ImmerState 的结构如下:

  const state: ProxyState = {
    type_: isArray ? ArchType.Array : (ArchType.Object as any),
    // Track which produce call this is associated with.
    scope_: parent ? parent.scope_ : getCurrentScope()!,
    // True for both shallow and deep changes.
    modified_: false,
    // Used during finalization.
    finalized_: false,
    // Track which properties have been assigned (true) or deleted (false).
    assigned_: {},
    // The parent draft state.
    parent_: parent,
    // The base state.
    base_: base,
    // The base proxy.
    draft_: null as any, // set below
    // The base copy with any updated values.
    copy_: null,
    // Called by the `produce` function.
    revoke_: null as any,
    isManual_: false
  }

createProxyProxy

创建一个 state 变量,然后根据 object/array 类型将 state 进行相应的 proxy 化(创建一个可撤回的 proxy), 挂在自身的 state.draft_ 属性上,同时返回该 proxy。

objectTraps

对属性访问的处理函数 handler,其中禁止了 defineProperty、setPropertyOf 等方法。 除了 get、set、deleteProperty 三处包含了核心功能之外,其余方法与原生无大差异。

src/core/scope.ts

每个 scope 表示一次 produce 调用。Scope 的数据结构如下:

export interface ImmerScope {
  patches_?: Patch[]                // 更改历史
  inversePatches_?: Patch[]         // 翻转的更改历史
  canAutoFreeze_: boolean           // 能否自动冻结
  drafts_: any[]                    // 草稿数据,proxy 集合
  parent_?: ImmerScope              // 父级 scope
  patchListener_?: PatchListener    // 更改监听器
  immer_: Immer                     // 当前 immer 实例,方便在任何位置访问
  unfinalizedDrafts_: number        // 未完成的草稿数据数量
}

getCurrentScope

获取当前 scope。

createScope

创建 scope。

usePatchesInScope

当 patchListense 存在时,初始化 scope.patches_ 以及 scope.inversePatches_。

revokeScope

同时将 scope.drafts_ 中的代理对象全部撤回(调用 revoke_ 方法或者将 revoke_ 置为 true)且将 scope.drafts_ 置为 null。

leaveScope

离开当前的 scope,回到父级 scope。

enterScope

使 currentScope 指向由 createScope 创建的 scope,并返回 currentScope。

revokeDraft

撤回 draft 状态,当代理对象是对象/数组时,使用 Proxy.revoke 撤回代理, 否则将 draft[DRAFT_STATE].revoked_ 置为 true,标记为已撤回。

src/core/current.ts

获取当前草稿的快照,并将其变为最终状态(但不冻结)。current 函数的输出可以安全地在 produce 之外使用。

current

不是草稿类型数据即报错,否则执行 currentImpl 函数。

currentImpl

递归的浅/深拷贝草稿,并返回拷贝后的新数据,即草稿的快照。

src/core/finalize.ts

用来将草稿数据最终化(冻结)。

processResult

取 scope.drafts_[0] 为 baseDraft_(通常为根 proxy 对象), 如果 recipe(proxy) 返回为 undefined 或者返回 baseDraft_ 时,将 baseDraft_ 进行最终化处理(冻结)。 同时重置 scope,将 scope 上的 patches_ 和 inversePatches_ 传入 patchListenser_ 执行(如果存在的话)。

finalize & finalizeProperty

对数据进行赋值以及冻结操作。同时在 scope 上生成 patches_ 以及 inversePatches_。

src/plugins/patches.ts

针对数据的更改,生成符合类似 JSON Patch 的数据(为了便于操作,op 为数组而不是 string)。 immer.js 中 Patch 的定义如下:

export interface Patch {
  op: "replace" | "remove" | "add"
  path: (string | number)[]
  value?: any
}

generateArrayPatches

针对数组类型的数据生成 patches_ 和 inversePatches_。若 base_ 比 copy_ 长,互换两者, 同时互换 patches_ 与 inversePatches_,以首先确保 base_ 比 copy_ 短,在此基础上进行后续的操作。 0 到 base_.length 的部分生成 op 为 replace 的 patch,base_.length 到 copy_.length 的部分生成 op 为 add 的 patch, copy_.length 到 base_.length 的部分生成 op 为 remove 的 patch。

generatePatchesFromAssigned

针对对象类型的数据生成 patches_ 和 inversePatches_。assigned_ 中无值为 remove, 否则 base_ 有值为 replace,base_ 无值为 add。

generateReplacementPatches

生成 replace 类型的 patch,添加进 patches_ 与 inversePatches_ 的 patch.value 刚好相反。

applyPatches

在 draft 上应用 patches,以生成草稿的新状态。首先对 patches 做了一些检查,以避免修改原型链等属性。 之后根据 patch 的 op 属性以及父级的数据类型进行增删改,最后返回 draft。