资讯专栏INFORMATION COLUMN

immer源码阅读

JerryZou / 2719人阅读

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

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://www.ucloud.cn/yun/96341.html

相关文章

  • immer.js 实战讲解文档

    摘要:无奈网络上完善的文档实在太少,所以自己写了一份,本篇文章以贴近实战的思路和流程,对进行了全面的讲解。这使得成为了真正的不可变数据。的使用非常灵活,多多思考,相信你还可以发现更多其他的妙用参考文档官方文档 文章在 github 开源, 欢迎 Fork 、Star 前言 Immer 是 mobx 的作者写的一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成...

    zhiwei 评论0 收藏0
  • 精读《源码学习》

    摘要:精读原文介绍了学习源码的两个技巧,并利用实例说明了源码学习过程中可以学到许多周边知识,都让我们受益匪浅。讨论地址是精读源码学习如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。 1. 引言 javascript-knowledge-reading-source-code 这篇文章介绍了阅读源码的重要性,精读系列也已有八期源码系列文章,分别是: 精读《Immer.js》源...

    aboutU 评论0 收藏0
  • immer.js 简介及源码解析

    摘要:例如维护一份在内部,来判断是否有变化,下面这个例子就是一个构造函数,如果将它的实例传入对象作为第一个参数,就能够后面的处理对象中使用其中的方法上面这个构造函数相比源代码省略了很多判断的部分。 showImg(https://segmentfault.com/img/bV27Dy?w=1400&h=544); 博客链接:下一代状态管理工具 immer 简介及源码解析 JS 里面的变量类...

    Profeel 评论0 收藏0
  • Immer.js简析

    摘要:所以整个过程只涉及三个输入状态,中间状态,输出状态关键是是如何生成,如何应用修改,如何生成最终的。至此基本把上的模式解析完毕。结束实现还是相当巧妙的,以后可以在状态管理上使用一下。 开始 在函数式编程中,Immutable这个特性是相当重要的,但是在Javascript中很明显是没办法从语言层面提供支持,但是还有其他库(例如:Immutable.js)可以提供给开发者用上这样的特性,所...

    Aceyclee 评论0 收藏0

发表评论

0条评论

JerryZou

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<