近期看了一下immer的源码,了解了一下原理,大致做一下记录
我们都知道immer是用来实现不可变数据的,比如一个对象A={a1:1,a2:{b:2}}, 使用immer去修改其中的a1的值,最终得到的新的值的a2成员与原A的a2成员是完全一样的,相比于cloneDeep 深拷贝来讲,节省了开销,相对性能较优。
这里我暂时只分享一下自己对immer的核心api 也就是produce函数的执行流程的学习记录。
produce
produce 函数有两种调用方式,科里化掉用和普通的函数调用方式。
// produce的第三个参数patcher函数,非核心功能,暂不看。作用是监听每次draft的变化。
// 科里化调用
const nextState = produce((draft) => draft.a = 1)({a: 0})
// 普通函数调用
const nextState = produce({a: 0}, (draft) => draft.a = 1)
源码
// 参数 base: 传进入的原始对象,如{a: 1},recipe: 具体的操作函数, patchListener 不议
produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
// 判断base是否是函数类型,若是函数类型,则代表是科里化掉用,进入if条件,最终转换为普通调用
if (typeof base === "function" && typeof recipe !== "function") {
const defaultBase = recipe
recipe = base
const self = this
return function curriedProduce(
this: any,
base = defaultBase,
...args: any[]
) {
return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args)) // prettier-ignore
}
}
// 传入的执行函数不是一个函数,报错
if (typeof recipe !== "function") die(6)
if (patchListener !== undefined && typeof patchListener !== "function")
die(7)
let result
// 只有纯对象/数组/"immerable classes" 才能被代理,若是,则进入if条件
// Only plain objects, arrays, and "immerable classes" are drafted.
if (isDraftable(base)) {
// 创建scope 用于处理嵌套 如{a: {b: {c: 1}, b2: {d: 2}}}
const scope = enterScope(this)
// 给传入的base对象设置代理 (ES6 Proxy), 返回一个proxy对象
const proxy = createProxy(this, base, undefined)
let hasError = true
try {
// 没有报错,则执行用户传入的执行函数,
// 注意,这里传入的参数是刚刚代理返回的对象,而不是原始的base对象
result = recipe(proxy)
hasError = false
} finally {
// 完成,撤销代理
// finally instead of catch + rethrow better preserves original stack
if (hasError) revokeScope(scope)
else leaveScope(scope)
}
// 执行函数的执行结果是Promise, 则等待执行
if (typeof Promise !== "undefined" && result instanceof Promise) {
return result.then(
result => {
usePatchesInScope(scope, patchListener)
// 处理结果,返回最终的nextState
return processResult(result, scope)
},
error => {
revokeScope(scope)
throw error
}
)
}
usePatchesInScope(scope, patchListener)
// 处理结果,返回最终的nextState
return processResult(result, scope)
} else if (!base || typeof base !== "object") {
result = recipe(base)
if (result === NOTHING) return undefined
if (result === undefined) result = base
if (this.autoFreeze_) freeze(result, true)
return result
} else die(21, base)
}
总结一下流程: 判断调用方式,科里化则转换为普通调用方式 -> 创建scope -> 给base设置代理,返回proxy代理对象 -> 执行传入的执行函数 -> 处理最终的结果 -> 返回
也就是将传入的base对象经过内部的处理,转换为了proxy对象,然后用传入的执行函数对这个proxy对象进行了操作,改变了proxy对象的一些数据(而非原始的base),最终再调用processResult,将proxy处理回我们想要的nextState。
核心点就是 proxy 代理是怎么做的,get set 的处理函数是怎么样的; 最终的结果处理是怎么做的。
proxy 拦截函数
这里就只看get 和set 这两个最主要的拦截方法了
export const objectTraps: ProxyHandler<ProxyState> = {
// get 拦截读取操作
get(state, prop) {
// DRAFT_STATE 是immer 内置的一个symbol, 当读取的属性DRAFT_STATE时,返回整个state
if (prop === DRAFT_STATE) return state
// latest的作用是 若有_copy则返回_copy, 没有copy过,则返回原始对象 _base,
// 这里意思就是获取最新的state
const source = latest(state)
// 如果没有这个属性。就去原型上找,返回原型上的描述对象
if (!has(source, prop)) {
return readPropFromProto(state, source, prop)
}
// 获取state里面当前需要读取的属性的最新值
const value = source[prop]
// 如果值已经finalized或者不可代理就返回
if (state.finalized_ || !isDraftable(value)) {
return value
}
// 完全相等,说明这个值没有代理过,代理,返回代理对象
if (value === peek(state.base_, prop)) {
prepareCopy(state)
return (state.copy_![prop as any] = createProxy(
state.scope_.immer_,
value,
state
))
}
// 正常返回值
return value
},
// set 拦截赋值操作
set(
state: ProxyObjectState,
prop: string /* strictly not, but helps TS */,
value
) {
const desc = getDescriptorFromProto(latest(state), prop)
// 原本就有setter,调用它自己的
if (desc?.set) {
desc.set.call(state.draft_, value)
return true
}
// modified_为fasle表示这个对象从来没有被修改过
if (!state.modified_) {
// 获取state里面这个属性的最新值
const current = peek(latest(state), prop)
const currentState: ProxyObjectState = current?.[DRAFT_STATE]
// 如果最新的值是一个代理对象,且代理对象_base的值就是设置的值,重置copy_
// special case, if we assigning the original value to a draft,
// we can ignore the assignment
if (currentState && currentState.base_ === value) {
state.copy_![prop] = value
// 这里置为false 没想明白为啥,有大佬知道还希望告知一下~
state.assigned_[prop] = false
return true
}
// 值没变, 跳过,不用赋值
if (is(value, current) && (value !== undefined || has(state.base_, prop)))
return true
//浅拷贝一份 到copy_
prepareCopy(state)
// 标记为modified,代表被修改过
markChanged(state)
}
// 如果都是NaN,或者原本值和要设置的值完全一样,就跳过,不用赋值
if (
state.copy_![prop] === value &&
// special case: NaN
typeof value !== "number" &&
// special case: handle new props with value 'undefined'
(value !== undefined || prop in state.copy_)
)
return true
state.copy_![prop] = value
state.assigned_[prop] = true
return true
}
}
proxy对象里面有几个比较重要的属性:base_ 传入的原对象, copy_原对象的拷贝,modified_ 是否被修改
总结一下,这里最需要注意的就是,我们传到produce函数里的执行函数,所接收到的draft,实际上是我们上一步创建出来的proxy对象,并不是原来传进去的对象。这里的增删改查,也就是针对原本的proxy对象的。在get中,我们看到有一个DRAFT_STATE变量,如果读取的属性是DRAFT_STATE,那么就会返回完整的proxy,否则就进行正常的get拦截,这里面immer还是懒代理来处理的,若从头至尾没有读取到的,也就不会浪费性能进行代理。而set函数,也是针对proxy进行的一系列判断和赋值,若正常的调用了set函数,就会把当前proxy的modified置为true(markChanged函数递归)。
解析结果
export function processResult(result: any, scope: ImmerScope) {
// 需要结果处理的数量
scope.unfinalizedDrafts_ = scope.drafts_.length
// 最开始创建的那个根scope
const baseDraft = scope.drafts_![0]
// 执行函数体里面手动return了
const isReplaced = result !== undefined && result !== baseDraft
if (!scope.immer_.useProxies_) // es5环境
getPlugin("ES5").willFinalizeES5_(scope, result, isReplaced)
if (isReplaced) {
//虽然 Immer 的 Example 里都是建议用户在 recipe 里直接修改 draft,但用户也可以选择在 recipe 最后返回一个 result,不过得注意“修改 draft”和“返回新值”这个两个操作只能任选其一,同时做了的话processResult函数就会抛出错误
if (baseDraft[DRAFT_STATE].modified_) {
revokeScope(scope)
die(4)
}
// return 出来的值就是一个proxy对象,如 return draft
if (isDraftable(result)) {
//核心处理,见下面
result = finalize(scope, result)
if (!scope.parent_) maybeFreeze(scope, result)
}
if (scope.patches_) {
getPlugin("Patches").generateReplacementPatches_(
baseDraft[DRAFT_STATE],
result,
scope.patches_,
scope.inversePatches_!
)
}
} else {
//核心处理,见下面
// Finalize the base draft.
result = finalize(scope, baseDraft, [])
}
// 解析完了之后对draft里面的批量取消代理
revokeScope(scope)
// patches 暂不考虑
if (scope.patches_) {
scope.patchListener_!(scope.patches_, scope.inversePatches_!)
}
// 返回最终结果
return result !== NOTHING ? result : undefined
}
function finalize(rootScope: ImmerScope, value: any, path?: PatchPath) {
// 已经frozen 直接返回
// Don't recurse in tho recursive data structures
if (isFrozen(value)) return value
const state: ImmerState = value[DRAFT_STATE]
// finalizeProperty函数 对子属性进行递归解析
// A plain object, might need freezing, might contain drafts
if (!state) {
each(
value,
(key, childValue) =>
finalizeProperty(rootScope, state, value, key, childValue, path),
true // See #590, don't recurse into non-enumarable of non drafted objects
)
return value
}
// Never finalize drafts owned by another scope.
// 在其他级别的scope中
if (state.scope_ !== rootScope) return value
// Unmodified draft, return the (frozen) original
// 没有被修改过,直接返回原对象
if (!state.modified_) {
maybeFreeze(rootScope, state.base_, true)
return state.base_
}
// 没有最终解析,就解析
// Not finalized yet, let's do that now
if (!state.finalized_) {
state.finalized_ = true
// 未解析数 - 1
state.scope_.unfinalizedDrafts_--
// 我们可以认为result实际上就是copy_,下面each函数,把result传进去,内部实际上拿到的就是父级的copy_
const result =
// For ES5, create a good copy from the draft first, with added keys and without deleted keys.
state.type_ === ProxyType.ES5Object || state.type_ === ProxyType.ES5Array
? (state.copy_ = shallowCopy(state.draft_))
: state.copy_
// Finalize all children of the copy
// For sets we clone before iterating, otherwise we can get in endless loop due to modifying during iteration, see #628
// Although the original test case doesn't seem valid anyway, so if this in the way we can turn the next line
// back to each(result, ....)
// 对每个属性都进行最终解析
each(
state.type_ === ProxyType.Set ? new Set(result) : result,
(key, childValue) =>
finalizeProperty(rootScope, state, result, key, childValue, path) // 内部同样调这个函数,递归
)
// everything inside is frozen, we can freeze here
maybeFreeze(rootScope, result, false)
// first time finalizing, let's create those patches
// patches暂不考虑
if (path && rootScope.patches_) {
getPlugin("Patches").generatePatches_(
state,
path,
rootScope.patches_,
rootScope.inversePatches_!
)
}
}
return state.copy_
}
最终解析结果,实际上是递归的调用了解析函数(finalize和finalizeProperty函数),这里没有贴出finalizeProperty,其内部主要就是递归调用finalize,并将结果赋值给上一级的copy_,就这样一级一级,最终赋值到根属性的copy_, 根属性的copy_就是最终的结果。
小结
核心:内部维护了一个对象,并利用proxy对这个对象进行代理,拦截执行函数中对它进行的读写操作,在这个对象里维护了很多标志位,用于在最终解析时识别,判断是否需要解析,是否可以直接返回等。由于是懒代理,只有真正读到了某个属性,才会对它进行代理,性能上更优。最终的解析,实际上是一步步递归赋值到父级的copy_上,直到根属性,拿到最终的值。
这样就把produce的主流程执行完了,学习记录,如有不妥或者不正确的,欢迎指正~