取代深克隆cloneDeep的方法 --- immer

参考阅读:https://juejin.im/post/5c079f9b518825689f1b4e88

一、使用

官网:https://immerjs.github.io/immer/docs/introduction

import produce from 'immer'
 
const nextData = produce(原始data, (data) => {
  // 尽情的修改data把
})

demo:

import produce from 'immer'

let currentState = {
  a: [],
  p: {
    x: 1
  }
}

let nextState = produce(currentState, (draftState) => {
  draftState.a.push(2);
})

二、原理

看代码:

produce(base: any, recipe?: any, patchListener?: any) {
    // ...省略一些前置参数判断,就是参数传错了就报错
    let result

    // 只有 plain objects、arrays 和 "immerable classes" 能被 drafted
    if (isDraftable(base)) {
        // 在 ImmerScope.current(static) 创建并保存当然作用域,以后用于存放draft
        const scope = ImmerScope.enter(this)
        // 创建 Proxy 实例供第二个参数(recipe)任意修改
        const proxy = this.createProxy(base, undefined)

        let hasError = true
        try {
            // 在 recipe 里面,用户做可以任意修改
            result = recipe(proxy)
            hasError = false
        } finally {
            // 如果出错,则销毁 proxy
            if (hasError) scope.revoke()
            else scope.leave()
        }

        if (typeof Promise !== "undefined" && result instanceof Promise) {
            return result.then(
                result => {
                    scope.usePatches(patchListener)
                    return processResult(this, result, scope)
                },
                error => {
                    scope.revoke()
                    throw error
                }
            )
        }
        
        // 挂载钩子(可以获得change的动作)
        scope.usePatches(patchListener)
        // 最后输出修改后的结果
        return processResult(this, result, scope)
    } else {
        result = recipe(base)
        if (result === NOTHING) return undefined
        if (result === undefined) result = base
        maybeFreeze(this, result, true)
        return result
    }
}
createProxy<T extends Objectish>(
    value: T,
    parent?: ImmerState
): Drafted<T, ImmerState> {
    // draft 就是 proxy
    const draft: Drafted = isMap(value)
        ? proxyMap(value, parent)
        : isSet(value)
        ? proxySet(value, parent)
        : this.useProxies
        ? createProxy(value, parent) // 一般 plain object 会在这里处理 p
        : createES5Proxy(value, parent)
    // ImmerScope.current 在这里存放 draft
    const scope = parent ? parent.scope : ImmerScope.current!
    scope.drafts.push(draft)
    return draft
}

function createProxy<T extends Objectish>(
    base: T,
    parent?: ImmerState
): Drafted<T, ProxyState> {
    const isArray = Array.isArray(base)
    const state: ProxyState = {
        type: isArray ? ProxyType.ProxyArray : (ProxyType.ProxyObject as any),
        // 作用域
        scope: parent ? parent.scope : ImmerScope.current!,
        // 是否被改动过
        modified: false,
        finalized: false,
        assigned: {},
        parent,
        // 传进来的原始数据对象
        base,
        // 基于本身建立的proxy
        draft: null as any, // set below
        // Any property proxies.
        drafts: {},
        // 被修改后,最终输出就存放在copy属性
        copy: null,
        // 存放“销毁”的方法
        revoke: null as any,
        isManual: false
    }

    let target: T = state as any
    // objectTraps 作为 Proxy 的处理器
    let traps: ProxyHandler<object | Array<any>> = objectTraps
    if (isArray) {
        target = [state] as any
        traps = arrayTraps
    }

    // 跟 new Proxy 差不多,但是返回多一个 revoke,可以用来销毁 proxy,节省内存空间
    // 基于target(就是state),来建立 proxy
    const {revoke, proxy} = Proxy.revocable(target, traps)
    state.draft = proxy as any
    state.revoke = revoke
    return proxy as any
}

const objectTraps: ProxyHandler<ProxyState> = {
    get(state, prop) {
        // 最后输出的时候,直接返回整个 state
        if (prop === DRAFT_STATE) return state
        let {drafts} = state

        if (!state.modified && has(drafts, prop)) {
            return drafts![prop as any]
        }

        const value = latest(state)[prop]
        if (state.finalized || !isDraftable(value)) {
            return value
        }

        if (state.modified) {
            if (value !== peek(state.base, prop)) return value
            // @ts-ignore 用于忽略ts报错
            drafts = state.copy
        }

        // 这里和 Vue 响应式原理差不多,set 之前必定先触发 get,触发 get 的时候,把改属性值也变成 Proxy
        return (drafts![prop as any] = state.scope.immer.createProxy(value, state))
    },
    has(state, prop) {
        return prop in latest(state)
    },
    ownKeys(state) {
        return Reflect.ownKeys(latest(state))
    },
    set(state, prop: string /* strictly not, but helps TS */, value) {
        if (!state.modified) {
            const baseValue = peek(state.base, prop)
            const isUnchanged = value
                ? is(baseValue, value) || value === state.drafts![prop]
                : is(baseValue, value) && prop in state.base
            if (isUnchanged) return true
            // 给 state.copy 浅复制一层
            prepareCopy(state)
            markChanged(state)
        }
        state.assigned[prop] = true
        // 这里用 copy 属性记录修改
        // @ts-ignore 用于忽略ts报错
        state.copy[prop] = value
        return true
    },
    deleteProperty(state, prop: string) {
        if (peek(state.base, prop) !== undefined || prop in state.base) {
            state.assigned[prop] = false
            prepareCopy(state)
            markChanged(state)
        } else if (state.assigned[prop]) {
            delete state.assigned[prop]
        }
        // 这里在 copy 属性记录修改
        // @ts-ignore 用于忽略ts报错
        if (state.copy) delete state.copy[prop]
        return true
    },
    getOwnPropertyDescriptor(state, prop) {
        const owner = latest(state)
        const desc = Reflect.getOwnPropertyDescriptor(owner, prop)
        if (desc) {
            desc.writable = true
            desc.configurable =
                state.type !== ProxyType.ProxyArray || prop !== "length"
        }
        return desc
    },
    defineProperty() {
        throw new Error("Object.defineProperty() cannot be used on an Immer draft") // prettier-ignore
    },
    getPrototypeOf(state) {
        return Object.getPrototypeOf(state.base)
    },
    setPrototypeOf() {
        throw new Error("Object.setPrototypeOf() cannot be used on an Immer draft") // prettier-ignore
    }
}

function processResult(immer: Immer, result: any, scope: ImmerScope) {
    // 拿到 draft(就是我们的proxy)
    const baseDraft = scope.drafts[0]
    // 根元素是否被换掉了
    const isReplaced = result !== undefined && result !== baseDraft
    immer.willFinalize(scope, result, isReplaced)
    if (isReplaced) {
        if (baseDraft[DRAFT_STATE].modified) {
            scope.revoke()
            throw new Error("An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.") // prettier-ignore
        }
        if (isDraftable(result)) {
            // Finalize the result in case it contains (or is) a subset of the draft.
            result = finalize(immer, result, scope)
            if (!scope.parent) maybeFreeze(immer, result)
        }
        if (scope.patches) {
            scope.patches.push({
                op: "replace",
                path: [],
                value: result
            })
            scope.inversePatches!.push({
                op: "replace",
                path: [],
                value: baseDraft[DRAFT_STATE].base
            })
        }
    } else {
        // plain object 的时候走这里
        result = finalize(immer, baseDraft, scope, [])
    }
    scope.revoke()
    if (scope.patches) {
        scope.patchListener!(scope.patches, scope.inversePatches!)
    }
    return result !== NOTHING ? result : undefined
}

function finalize(
    immer: Immer,
    draft: Drafted,
    scope: ImmerScope,
    path?: PatchPath
) {
    const state = draft[DRAFT_STATE]
    if (!state) {
        if (Object.isFrozen(draft)) return draft
        return finalizeTree(immer, draft, scope)
    }
    if (state.scope !== scope) {
        return draft
    }
    if (!state.modified) {
        maybeFreeze(immer, state.base, true)
        return state.base
    }
    if (!state.finalized) {
        state.finalized = true
        finalizeTree(immer, state.draft, scope, path)
        if (immer.onDelete && state.type !== ProxyType.Set) {
            if (immer.useProxies) {
                const {assigned} = state
                each(assigned, (prop, exists) => {
                    if (!exists) immer.onDelete!(state, prop as any)
                })
            } else {
                const {base, copy} = state
                each(base, prop => {
                    if (!has(copy, prop)) immer.onDelete!(state, prop as any)
                })
            }
        }
        if (immer.onCopy) {
            immer.onCopy(state)
        }

        if (immer.autoFreeze && scope.canAutoFreeze) {
            freeze(state.copy, false)
        }

        if (path && scope.patches) {
            generatePatches(state, path, scope.patches, scope.inversePatches!)
        }
    }
    // 最后返回 proxy 的 copy 作为结果
    return state.copy
}

 

posted @ 2020-02-12 14:43  张啊咩  阅读(1306)  评论(0编辑  收藏  举报