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);
介于上述原因,社区提供了一个适配 React 且更改复杂数据非常便捷的库 immer.js。
创建 scope,创建 proxy,调用 draft 的 change handler 生成最终 result,最后调用 patchListenser 以及冻结 result 并返回。
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
}
创建一个 state 变量,然后根据 object/array 类型将 state 进行相应的 proxy 化(创建一个可撤回的 proxy), 挂在自身的 state.draft_ 属性上,同时返回该 proxy。
对属性访问的处理函数 handler,其中禁止了 defineProperty、setPropertyOf 等方法。 除了 get、set、deleteProperty 三处包含了核心功能之外,其余方法与原生无大差异。
getter
这里只会对访问到的属性进行 proxy 代理,而不是采取所有属性全代理模式,执行效率较高。
get(state, prop) {
...
if (value === peek(state.base_, prop)) {
// getter 将 base_ 值浅复制到 state.copy_ 上
prepareCopy(state)
// 仅对访问到的 prop 将 value 也进行代理,挂在 state.copy_[prop] 上
return (state.copy_![prop as any] = createProxy(value, state))
}
}
setter
set(state, prop, value) {
if (!state.modified_) {
// 递归的将 state.modified_ 及 state.parent_.modifield_ 全部置为 true
// 标记为已更改
markChanged(state)
}
// 检查新旧 value 相同时跳过
if ((state.copy_![prop] === value) return true;
// 设置新值,同时在 state.assigned_ 上记录下更改的属性 prop
state.copy_![prop] = value
state.assigned_[prop] = true
return true
}
deleteProperty
deleteProperty(state, prop) {
if (state.base_[prop]) {
state.assigned_[prop] = false
} else {
// 没有赋过值,直接删除时
delete state.assigned_[prop]
}
delete state.copy_[prop]
}
每个 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 // 未完成的草稿数据数量
}
获取当前 scope。
创建 scope。
当 patchListense 存在时,初始化 scope.patches_ 以及 scope.inversePatches_。
同时将 scope.drafts_ 中的代理对象全部撤回(调用 revoke_ 方法或者将 revoke_ 置为 true)且将 scope.drafts_ 置为 null。
离开当前的 scope,回到父级 scope。
使 currentScope 指向由 createScope 创建的 scope,并返回 currentScope。
撤回 draft 状态,当代理对象是对象/数组时,使用 Proxy.revoke 撤回代理, 否则将 draft[DRAFT_STATE].revoked_ 置为 true,标记为已撤回。
获取当前草稿的快照,并将其变为最终状态(但不冻结)。current 函数的输出可以安全地在 produce 之外使用。
不是草稿类型数据即报错,否则执行 currentImpl 函数。
递归的浅/深拷贝草稿,并返回拷贝后的新数据,即草稿的快照。
用来将草稿数据最终化(冻结)。
取 scope.drafts_[0] 为 baseDraft_(通常为根 proxy 对象), 如果 recipe(proxy) 返回为 undefined 或者返回 baseDraft_ 时,将 baseDraft_ 进行最终化处理(冻结)。 同时重置 scope,将 scope 上的 patches_ 和 inversePatches_ 传入 patchListenser_ 执行(如果存在的话)。
对数据进行赋值以及冻结操作。同时在 scope 上生成 patches_ 以及 inversePatches_。
针对数据的更改,生成符合类似 JSON Patch 的数据(为了便于操作,op 为数组而不是 string)。 immer.js 中 Patch 的定义如下:
export interface Patch {
op: "replace" | "remove" | "add"
path: (string | number)[]
value?: any
}
针对数组类型的数据生成 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。
针对对象类型的数据生成 patches_ 和 inversePatches_。assigned_ 中无值为 remove, 否则 base_ 有值为 replace,base_ 无值为 add。
生成 replace 类型的 patch,添加进 patches_ 与 inversePatches_ 的 patch.value 刚好相反。
在 draft 上应用 patches,以生成草稿的新状态。首先对 patches 做了一些检查,以避免修改原型链等属性。 之后根据 patch 的 op 属性以及父级的数据类型进行增删改,最后返回 draft。