immer源码阅读

immer是前端在immutable领域的另外一个实践,相比较immutable而言,它拥有更低的学习成本,在使用上可以直接使用js 原生api去修改引用对象,得到一个新的不可变的引用对象。

import produce from "immer"

const baseState = [
    {
        todo: "Learn typescript",
        done: true
    },
    {
        todo: "Try immer",
        done: false
    }
]

const nextState = produce(baseState, draftState => {
    draftState.push({todo: "Tweet about it"})
    draftState[1].done = true
})

immer的实现主要有两种方案,在支持Proxy的环境下会使用Proxy,在不支持Proxy的环境下会使用defineProperty。在这里主要介绍Proxy的实现方案,因为基本的实现思路都是相似的,通过学习Proxy的实现方案,我们也能熟悉一下在平时业务开发时很少用的Proxy api。

从上面的例子可以看出使用immer,主要通过调用produce这个api,从源码中可以看到produce这个函数其实调用了produceProxy函数:

function produce(baseState, producer) {
    ...
    return getUseProxies()
        ? produceProxy(baseState, producer)
        : produceEs5(baseState, producer)
}

produceProxy

在继续阅读immer的源码之前,我们不妨想一下,如何通过Proxy实现immer的功能?

我们必须在没修改对象的情况下获取原对象的属性,在修改的情况下又不要修改原对象的属性。我们可以很容易想到get handler的操作:

new Proxy(data, {
  get(target, prop){
    return target[prop]
  },
  set(target, prop, value){
    
  }
})

但是set如何处理?所以我们代理的对象不能只是数据本身,在immer中每个代理的对象都是以下结构:

function createState(parent, base) {
    return {
        base,            // 要代理的原数据
        parent,          // 要代理数据的父对象
        copy: undefined,    // 在set时,修改这个数据对应的值
        proxies: {},        // 讲解get时,再谈这个
        modified: false,    // 有没有要修改这份数据
        finalized: false    //  本文最后会讲解
    }
}

接下来我们回到produceProxy函数:

export function produceProxy(baseState, producer) {
    ...
    const previousProxies = proxies
    proxies = [] // 通过createProxy创建的proxy都会在这里面
    try {
        // create proxy for root
        const rootProxy = createProxy(undefined, baseState) // 创建根代理
        // execute the thunk
        const returnValue = producer.call(rootProxy, rootProxy) // 执行函数,拿到返回值
        // and finalize the modified proxy
        let result
        // check whether the draft was modified and/or a value was returned
        if (returnValue !== undefined && returnValue !== rootProxy) {
            ...
        } else {
            result = finalize(rootProxy)
        }
        // revoke all proxies
        each(proxies, (_, p) => p.revoke())  // 销毁代理,主要是为了防止外层的变量拿到这个代理做一些操作
        return result
    } finally {
        proxies = previousProxies
    }
}

可以看到主要逻辑还是很清晰的,为数据创建代理,然后调用producer函数,最后finalize(rootProxy)。

接下来看一下createProxy的相关逻辑:

function createProxy(parentState, base) {
    if (isProxy(base)) throw new Error("Immer bug. Plz report.")
    const state = createState(parentState, base)
    const proxy = Array.isArray(base)
        ? Proxy.revocable([state], arrayTraps)
        : Proxy.revocable(state, objectTraps)
    proxies.push(proxy)
    return proxy.proxy
}

createProxy的逻辑看起来很简单,但是你可能会有两个疑问:

  • 为什么使用Proxy.revocable做代理,而不是new Proxy?
  • 为什么要把数组的state包裹到一个数组里面[state]

先来回答第一个问题,使用Proxy.revocable主要是为了防止以下情况的出现:

let proxy
const nextState = produce(baseState, s => {
    proxy = s
    s.aProp = "hello"
})
proxy.aProp = "Hallo"

如代码所示,如果produce执行完成后,proxy不做revoke,会导致外部变量拿到的proxy,还有作用,就会造成不期望的情况出现。所以在produceProxy最后,会把函数执行周期所有创建的proxy都revoke掉。

第二个问题,通过produceProxy的代码,我们可以看到在调用外部传入的producer函数的时候,传给producer函数的是proxy,如果不使用[state],proxy代理的state就是一个对象。此时如果对其类型进行判断Array.isArray(proxy)就会返回false。

我们可以看一下objectTraps和arrayTraps分别是什么:

const objectTraps = {
    get,
    has(target, prop) {
        return prop in source(target)
    },
    ownKeys(target) {
        return Reflect.ownKeys(source(target))
    },
    set,
    deleteProperty,
    getOwnPropertyDescriptor,
    defineProperty,
    setPrototypeOf() {
        throw new Error("Immer does not support `setPrototypeOf()`.")
    }
}

const arrayTraps = {}
each(objectTraps, (key, fn) => {
    arrayTraps[key] = function() {
        arguments[0] = arguments[0][0]
        return fn.apply(this, arguments) // state push proxy
    }
})

可以看到objectTraps是一个很普通的handlers,而arrayTraps则是在objectTraps上包裹了一层,传入的参数将[state]改为了state

Handler

接下来看一下get,先看一下数据没有被修改过的情况(即还没调用过set)

function get(state, prop) {
    if (prop === PROXY_STATE) return state  // PROXY_STATE是一个symbol值,有两个作用,一是便于判断对象是不是已经代理过,二是帮助proxy拿到对应state的值
    if (state.modified) {
        ...
    } else {
        if (has(state.proxies, prop)) return state.proxies[prop] 
        const value = state.base[prop]
        if (!isProxy(value) && isProxyable(value))
            return (state.proxies[prop] = createProxy(state, value))
        return value
    }
}

get函数主要有两个作用:

  • 返回对应的数据
  • 为对应的数据创建代理

通过get的时候创建代理就保证了不管在produce中操作的数据嵌套有多深,我们操作的都是代理对象,如:

a.b.c = 1           // a.b是一个代理对象
a.b.c.push(1)       // a.b.c是一个代理对象

接下来看set函数

function set(state, prop, value) {
    // set的关键是不改老的值,所以改的copy上的值
    if (!state.modified) {
        if (
            (prop in state.base && is(state.base[prop], value)) ||
            (has(state.proxies, prop) && state.proxies[prop] === value) //值不变的情况下直接return true
        )
            return true
        markChanged(state)
    }
    state.copy[prop] = value
    return true
}

set的逻辑相对简单,set值就是改state.copy上的值,同时如果state是第一次修改,就markChanged(state)

function markChanged(state) {
    if (!state.modified) {
        state.modified = true
        state.copy = shallowCopy(state.base)
        // copy the proxies over the base-copy
        Object.assign(state.copy, state.proxies) // yup that works for arrays as well
        if (state.parent) markChanged(state.parent)
    }
}

在markChanged函数中,把base的属性和proxies的上的属性都浅拷贝给了copy,从此,对目标对象的取值还是设值都是操作state.copy。

我们看一下整个get函数,可以看到state.modified为true的情况下,逻辑很简单,对应属性没变化的时候创建代理,返回值,对应属性变化了,直接返回对应值。

function get(state, prop) {
    if (prop === PROXY_STATE) return state
    if (state.modified) {
        const value = state.copy[prop]
        if (value === state.base[prop] && isProxyable(value))
            return (state.copy[prop] = createProxy(state, value))
        return value
    } else {
        if (has(state.proxies, prop)) return state.proxies[prop]
        const value = state.base[prop]
        if (!isProxy(value) && isProxyable(value))
            return (state.proxies[prop] = createProxy(state, value))
        return value
    }
}

finalize

除了get和set,还有6个其他的handler,但整体思路和get、set一致,就不一一介绍了。我们看一下produceProxy的最后一块,也是我认为最不好理解的一部分finalize。

export function produceProxy(baseState, producer) {
    ...
    const previousProxies = proxies
    proxies = [] // 通过createProxy创建的proxy都会在这里面
    try {
        // create proxy for root
        const rootProxy = createProxy(undefined, baseState) // 创建根代理
        // execute the thunk
        const returnValue = producer.call(rootProxy, rootProxy) // 执行函数,拿到返回值
        // and finalize the modified proxy
        let result
        // check whether the draft was modified and/or a value was returned
        if (returnValue !== undefined && returnValue !== rootProxy) {
            ...
        } else {
            result = finalize(rootProxy)
        }
        // revoke all proxies
        each(proxies, (_, p) => p.revoke())  // 销毁代理,主要是为了防止外层的变量拿到这个代理做一些操作
        return result
    } finally {
        proxies = previousProxies
    }
}

前面通过producer函数对rootProxy进行了一系列的操作,现在我们要返回下一次的state,我们要递归地state上的属性,把属性对应的代理对象,改为对应的值。

export function finalize(base) {
    if (isProxy(base)) {
        const state = base[PROXY_STATE]
        if (state.modified === true) {
            if (state.finalized === true) return state.copy 
            state.finalized = true
            return finalizeObject(
                useProxies ? state.copy : (state.copy = shallowCopy(base)),
                state
            )
        } else {
            return state.base
        }
    }
    finalizeNonProxiedObject(base) // base不是代理则说明base下面的属性会有代理
    return base
}

因为我们第一次传入finalize函数的是rootProxy,是一个Proxy,我们先看isProxy(base)为true的情况,简化一下对应的逻辑,可以看到逻辑很简单:

const state = base[PROXY_STATE]
if (state.modified === true) {
    return finalizeObject(
        state.copy,
        state
    )
} else {
    return state.base
}

如果state没有被修改过,就直接返回state.base,如果state修改过,就返回finalizeObject(state.copy, state)函数的返回值。

至于为什么要设置state.finalized的值,我们稍后再讲,我们先看一下finalizeObject函数的逻辑。

function finalizeObject(copy, state) {
    const base = state.base
    each(copy, (prop, value) => {
        if (value !== base[prop]) copy[prop] = finalize(value)
    })
    return freeze(copy)
}

finalizeObject函数遍历copy上的属性,对于value和base[prop]不相等的情况,调用finalize(value),最后freeze copy对象,然后返回。

value和base[prop]不相等说明可能存在两种情况:

  • 由于value被get过,此时value是一个代理对象。
  • value被set过,此时value可能是一个普通的值也可能是一个代理对象(比如把rootProxy的某个子孙代理属性赋值给了copy[prop],即value)。

所以我们就好理解finalize函数中为什么既要处理value是proxy的情况,又要处理value不是proxy的情况了。

当value不是一个proxy的时候,value的子属性可能是一个proxy(因为赋值的时候,可能值的子属性是proxy),immer用finalizeNonProxiedObject处理这种情况。

function finalizeNonProxiedObject(parent) {
    // If finalize is called on an object that was not a proxy, it means that it is an object that was not there in the original
    // tree and it could contain proxies at arbitrarily places. Let's find and finalize them as well
    if (!isProxyable(parent)) return
    if (Object.isFrozen(parent)) return
    each(parent, (i, child) => {
        if (isProxy(child)) {
            parent[i] = finalize(child)
        } else finalizeNonProxiedObject(child)
    })
    // always freeze completely new data
    freeze(parent)
}

如果属性值是一个proxy,就调用finalize,以去除proxy,否则就递归的去找下面属性是不是proxy。

最后我们说一下finalize函数中为什么要state.finalized = true,按照正常的逻辑属性在finalize函数中只会访问一次,根本这行代码。

这行代码是为了防止一种情况,某个属性值被赋值给了另外一个属性,这两个属性访问的是一个数据,此时如果state已经finalized,就直接返回他的copy。

至此,我们已经阅读完immer的核心逻辑,和所有比较难以理解的地方。

    原文作者:黑色的影子
    原文地址: https://segmentfault.com/a/1190000015796542
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞