immer 原理: immer 源码解读 (一)

709 阅读8分钟

近期看了一下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_
}

最终解析结果,实际上是递归的调用了解析函数(finalizefinalizeProperty函数),这里没有贴出finalizeProperty,其内部主要就是递归调用finalize,并将结果赋值给上一级的copy_,就这样一级一级,最终赋值到根属性的copy_, 根属性的copy_就是最终的结果。

小结

核心:内部维护了一个对象,并利用proxy对这个对象进行代理,拦截执行函数中对它进行的读写操作,在这个对象里维护了很多标志位,用于在最终解析时识别,判断是否需要解析,是否可以直接返回等。由于是懒代理,只有真正读到了某个属性,才会对它进行代理,性能上更优。最终的解析,实际上是一步步递归赋值到父级的copy_上,直到根属性,拿到最终的值。

这样就把produce的主流程执行完了,学习记录,如有不妥或者不正确的,欢迎指正~