immer 为什么具有高性能| 8月更文挑战

999 阅读4分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

在讲解 immer 高性能的原因之前,首先要普及一下 immer 的背景知识。

immerjs 是一个 基于 immutablejs 思想的一个库,用于解决在 redux 的 reducer 中处理数据异常复杂的问题。

下面我们通过一个例子看看 传统 redux 中 reducer 是怎么书写的:


const initialState = {

    value: 0

}

// reducer.js

export default function reducer(state, action) {

    const { type, payload } = action

    switch(type) {

        case 'add':

        return {

            ...state,

            value: state.value + payload

        }

    }

}

这个 reducer 用于对 value 进行累加操作。看起来好像不是很麻烦,那么我们换一个更贴近业务逻辑的例子。


const initialState = {

    user: {

        name: '',

        address: {

            home: '',

            company: '',

        }

    }

}

// reducer.js

export default function reducer(state, action) {

    const { type, newHomeAddress } = action

    switch(type) {

        case 'update-home-address':

        return {

            ...state,

            address: {

                ...state.address,

                home: newHomeAddress,

            }

        }

    }

}

从这个例子中,可以看到当 redux 中 state 的嵌套层级很深的时候,在处理 reducer 的返回数据逻辑时冗长而丑陋。

immer 就是为了解决这个问题而诞生的。

既然我们是想把 user.address.home 修改为 newHomeAddress,为什么我们需要不断的解构来实现呢,可不可以直接声明 user.address.home = newHomeAddress 呢。

当然是可以的。我们知道只需要把 对象经过 Proxy 代理一下,就可以解决这个问题。类似于 vue 中响应式的实现方式。

immer 也确实是这么做的,但在此基础上,可能又会产生新的问题。如果这个 user 下嵌套的层级非常深,我们一开始进行代理的时候去循环代码它的内层对象,就可能导致性能问题。如果不深层代理,就肯定会出现内层属性没有代理成功,从而导致 bug。

immer 是怎么解决这个问题的呢,这就是本篇文章要讲的重点。

immer 是通过 懒代理的方式去实现高性能的数据代理的。

懒代理,即用到的时候才代理。我们举个例子说明一下:


const user = {

    address: {

        home: '',

    }

}

如上这个user,immer 只会对 user 进行代理,当访问 user 时会触发 user 的 getter 方法,进而 immer 会对 user.address 也进行代理,以此类推,只有你访问到这个对象时,immer 才会对它内层的属性再次创建 Proxy 代理,这样就巧妙的解决了层层代理的性能问题。

先来使用 immer 改造一下上面的 reducer:


import produce from 'immer'



const initialState = {

    user: {

        name: '',

        address: {

            home: '',

            company: '',

        }

    }

}

// reducer.js

export default produce(function reducer(state, action) {

    const { type, newHomeAddress } = action

    switch(type) {

        case 'update-home-address':

            state.address.home = newHomeAddress

            break

    }

}, initialState)

可以看到,直接对 home 赋值的一行代码就实现了之前层层解构合并的功能。

下面我们看看源码中是怎么实现的。(略去次要部分)


function produce(base, recipe) {

    const proxy = createProxy(this, base, undefined)

}

function createProxy(immer, value, parent) {

    // only Array or Object run this

    const draft = createProxyProxy(value, parent)

    return draft

}

可以看到,传入 produce 的值,开始就做了代理相关的工作,createProxyProxy,我们展开看一下


function createProxyProxy(base, parent) {

    const state: ProxyState = {

        type_: isArray ? ProxyTypeProxyArray : (ProxyTypeProxyObject as any),

        scope_: parent ? parent.scope_ : getCurrentScope()!,

        modified_: false,

        finalized_: false,

        assigned_: {},

        parent_: parent,

        base_: base,

        draft_: null as any,

        copy_: null,

        revoke_: null as any,

        isManual_: false

    }

    let traps = objectTraps

    const { proxy } = Proxy.revocable(state, traps)

    state.draft = proxy

    state.revoke_ = revoke



    return proxy

}



首先创建了一个 state,这个对象上有很多属性,包括 需要代理的值 state,类型,父scope,是否已经结束,是否修改等等。

然后通过 Proxy.revocable 对这个 state 做了代理,与 new Proxy 不同的是,通过这种方式创建的代理是可以 revoke 的。

具体 revoke 的作用后文在分析。

可以看到,这里的关键点就在于 obejctTraps,因为代理的具体 get、set 还没有展开说。


const objectTraps = {

    get(state, prop) {

        const value = source[prop]

        if (!isDraftable(value)) return value

        prepareCopy(state)

        return (

            state.copy_[prop] = createProxy(

                state.scope_.immer_,

                value,

                state

            )

        )

    }

}

从这里可以看到,首先 immer 会拿到开发者想要取到的真实的值,然后会判断 isDraftable,这个方法其实是为了区分是否是基本数据类型,因为一些基本数据类型是不需要被代理的,所以直接返回。

这也就是 immer 在执行懒代理时,不会对所有内层属性都代理,只有这个内层属性是有数组、对象、Map、Set 等能够被代理的值才会代理。

然后 prepareCopy 的代码:


export function prepareCopy(state: {base_: any; copy_: any}) {

    if (!state.copy_) {

        state.copy_ = shallowCopy(state.base_)

    }

}

可以看到 prepareCopy 在 state 上绑定了一个原本数据的浅拷贝。

为什么要这么做呢,因为 immer 在懒代理的时候当然不能直接对访问的属性再次代理,因为这样的话 就是全量代理了。那么他先创建一个副本在 copy 属性上,然后当真正需要创建内层代理的时候,再对这个 copy 进行代理即可。

可以看到 prepareCopy 后面的代码,它分为两部分:


state.copy_[prop] = createProxy(

    state.scope_.immer_,

    value,

    state

)

return state.copy_[prop]

首先,对这个需要被代理的值创建了一个代理 放到了 copy[prop] 上。然后返回了这个 代理对象。

这样就保证了,当再次访问 深层属性的时候,可以被刚刚这个 state.copy_[prop] 的代理 getter 捕获到,如果深层又可以代理,再次重复 creteProxy 的逻辑。

至此,就实现了 immer 中对对象的懒代理,保证了代理的高性能。