AnathanPham / blog

随便写写
1 stars 0 forks source link

深入浅出Immer #85

Open AnathanPham opened 1 year ago

AnathanPham commented 1 year ago

简介

Immer是一个用于更新不可变数据的工具库。 你应该知道,React的state的一个非常重要的要求就是不可变。在这里我们不赘述为什么不可变数据对React如此重要。我们今天只对Immer进行深入的探究。 Immer最酷的一点是,能够让你用操作可变数据的方式来操作不可变的数据。 下面是官方文档给出的简单示例:https://immerjs.github.io/immer/ 第一眼的感觉便是难以置信,第二眼就会想他是如何做到的。

Immer实现的原理

Immer利用了Proxy的特性来实现的。 如果这样去给别人解释,自己心里都会没底气。Proxy,大家都知道它,来,你实现一个看看?😂 为了避免遇到上述的尴尬,同时作为一个自称是精通React的人,有必要深入了解一下React的好朋友Immer。

我们再来看一下Immer的使用方式:

import produce from "immer"

const baseState = [
    {
        title: "Learn TypeScript",
        done: true
    },
    {
        title: "Try Immer",
        done: false
    }
]

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

Immer在回调函数中,直接对变量进行更改,而不是我们熟悉的解构赋值。(从解构赋值到Immer,就像是石器时代进入到工业革命) 直觉告诉我们,Immer一定是捕获了我们赋值的操作,并相应了在内部创造了一个副本并按照我们的操作去修改了副本,最终返回了这个副本。

Produce方法

来看看produce做了什么:

produce: (base: any, recipe?: any) => {
    const proxy = createProxy(this, base, undefined)
    let result = recipe(proxy)
    return processResult(result, scope)
}

produce对原始state做了代理得到proxy,接着将proxy传入recipe(也就是我们对数据进行直接操作的回调函数)执行得到了result(result是一个被更新过的草稿,此时他依然是代理对象),最终调用processResult将result处理成被更新后的原始state的副本(这个结果,等价于我们通过解构赋值的方式获取的结果)

给你一分钟的时间想想,是不是有些不对劲? 看起来,Immer只代理了一层--原始对象的第一层属性。如果我这样a.b.c = 1操作呢?

createProxy--递归创建proxy

下面就要来看看Proxy是如何做到的

export function createProxyProxy(
    base,
    parent
){
    const state: ProxyState = {
        base_: base,
    }
    let target= state
    let traps = objectTraps

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

export const objectTraps: ProxyHandler<ProxyState> = {
    get(state, prop) {
        if (value === peek(state.base_, prop)) {
            prepareCopy(state)
            return (state.copy_![prop as any] = createProxy(
                state.scope_.immer_,
                value,
                state
            ))
        }
        return value
    },
}

看objectTraps的get方法,当你读原始state的属性的时候,他便开始工作。objectTraps.get顺着你读取属性的路径,递归的给子对象属性属性做代理。这样,不论多深的层级,都能被Immer代理上了。

createProxy--更新代理数据

当你修改数据的时候,对应的修改会更新到代理对象上。(在你修改前,get属性会先起作用,所以此时你修改的属性所属的对象已经被代理)

export const objectTraps: ProxyHandler<ProxyState> = {
    set(
        state: ProxyObjectState,
        prop: string,
        value
    ) {
        state.copy_![prop] = value
        return true
    },
}

markChanged 顺藤摸瓜

显然,递归遍历统计更新属性节点当然可行,但是这样对性能来说便是一个较大的负担。 Immer在set值的时候沿着属性节点的代理对象到顶部的代理对象一一进行了标记:

export const objectTraps = {
    set(
        state: ProxyObjectState,
        prop: string,
        value
    ) {
    if (!state.modified_) {
            markChanged(state)
        }
        state.copy_![prop] = value
        return true
    },
}
function markChanged(state: ImmerState) {
    if (!state.modified_) {
        state.modified_ = true
        if (state.parent_) {
            markChanged(state.parent_)
        }
    }
}

那么,Immer最后是如何统计哪些属性被更新的呢?难道是递归遍历所有属性吗?

recipe 修改代理对象

现在,我们直接在recipe中放心的修改数据。要记住,recipe返回的结果依然是一个代理对象。 接下来,Immer将会把代理对象还原为原始数据的修改后的副本...

processResult 还原对象

最终,processResult将代理对象还原。 因为已经标记了哪些属性被更新,所以未被更新的节点会被跳过。 遍历的过程是层序遍历,每次深入一层,都会创建一个当前层的浅拷贝对象副本,并修改他,接着进入下一层,直到对象副本被完全修改好,最后将对象副本返回。

参考