【源码共读】| Immer数据不可变

137 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

今天阅读的是:immer

github.com/immerjs/imm…

image-20230227161954444

  • immer是一个用来更方便的保证数据状态不可变的库
  • 与之类似的库还有immutable

尝试使用

假如我们有一个场景,需要对上面的第二个todo做修改,并新增一个todo item,但是又不希望对整个数组做深拷贝

const baseState = [
    {
        title: "Learn TypeScript",
        done: true
    },
    {
        title: "Try Immer",
        done: false
    }
]
  • 不使用immer
const nextState = baseState.slice() // 浅拷贝
nextState[1] = {
    ...nextState[1], // 浅拷贝
    done: true
}
// 违反了数据不可变性
nextState.push({title: "Tweet about it"})
  • 使用immer
import produce from "immer"

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

可以看出,immer更方便快捷的修改我们的数据,那么他是怎么实现的呢


原理分析

我们在其官网(immerjs.github.io/immer/)上可以看…

image-20230227165215920

  • immer中间有一层草稿层来对数据进行修改,那他是怎么创建出这个草稿层,从而实现精准更新的呢?

源码调试

我们可以通过他的一个例子来看看源码中是如何实现的

  1. 首先,我们打开package.json,查看入口文件,可以看到核心文件是immer.ts

image-20230227165711229

  1. 查看测试用例,我们以base.js为例
runBaseTest("proxy (no freeze)", true, false)
function runBaseTest(name, useProxies, autoFreeze, useListener) {
    const listener = useListener ? function() {} : undefined
    const {produce, produceWithPatches} = createPatchedImmer({
    useProxies,
    autoFreeze
})
    function createPatchedImmer(options) {
        // 创建一个immer 对象
        const immer = new Immer(options)
        // 省略...
    }
}
  1. 根据Immer对象,我们跳转到构造函数。
    • 这里主要的逻辑在produce中,我们来看看具体的实现

//  * @param {any} base - 原始数据
//  * @param {Function} recipe - 修改数据操作的函数
//  * @param {Function} patchListener - optional 回调函数(patch用来记录修改的步骤,以达到精准更新的目的)
//  * @returns {any} 返回新的数据状态,如果没有修改,则返回原始数据 
produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
    // 科里化调用
    if (typeof base === "function" && typeof recipe !== "function") {
        const defaultBase = recipe
        recipe = base

        const self = this // 保存当前的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类可以编辑
    if (isDraftable(base)) {
        // 使用scope 来区分不同的层级,以便处理嵌套数据中的层级关系
        const scope = enterScope(this)
        // immer的核心就是利用Proxy来对数据进行拦截
        const proxy = createProxy(this, base, undefined)
        let hasError = true
        try {
            // 将代理的对象传入操作的回调函数中,此时修改的数据即draft
            result = recipe(proxy)
            hasError = false
        } finally {
            // finally instead of catch + rethrow better preserves original stack
            // 并没有使用 catch & rethrow,而是利用了 finally 一定会被执行的特性
            if (hasError) revokeScope(scope)
            else leaveScope(scope)
        }
        // 支持使用Promise 异步操作
        if (typeof Promise !== "undefined" && result instanceof Promise) {
            return result.then(
                result => {
                    usePatchesInScope(scope, patchListener)
                    return processResult(result, scope)
                },
                error => {
                    // revoke (撤销)scope 中的 proxy
                    revokeScope(scope)
                    throw error
                }
            )
        }
        // 将patchListener存到scope中
        usePatchesInScope(scope, patchListener)
        // 根据patches的记录来处理更新
        return processResult(result, scope)
    } else if (!base || typeof base !== "object") {
        // 如果基础数据还没定义
        result = recipe(base)
        if (result === undefined) result = base
        if (result === NOTHING) result = undefined
        if (this.autoFreeze_) freeze(result, true)
        if (patchListener) {
            const p: Patch[] = []
            const ip: Patch[] = []
            getPlugin("Patches").generateReplacementPatches_(base, result, p, ip)
            patchListener(p, ip)
        }
        return result
    } else die(21, base)
}

  1. 总结
  • 通过上面的代码描述,我们可以知道:
  1. 流程图展示

具体的流程步骤,以一个流程图展示:

无标题-2023-02-28-0903

总结

  • immer使用了Proxy来做数据劫持,以达到精准的更新。对于简单的数据类型(如字符串,数字,布尔类型等)会直接使用recipe函数进行处理。
  • 当处理嵌套数据时,会使用scope来保存上下文,用来区分层级关系,利用patches来存储修改记录,方便进行精准的更新。
  • 数据劫持的形式不是直接new Proxy,而是使用了Proxy.revocable,他们之间的构造函数是相同的,只是多了一个撤销的函数(revoke),可以取消数据代理

参考资料