immer 原理

前言

掘金上的immer源码解析,根据的是以前的源码,跟现在的源码略有不同,大体思路上是可以参考的。大家自己看的话,最好还是以github上的源码为准,毕竟文章中的源码不知道什么时候就过时了。

下面的涉及的源码是7.0.9版本拉取的。

Proxy

immer 原理涉及到ES6的特性:Proxy

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

如下是对读取属性拦截的示例:

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

Proxy支持的拦截操作:

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。

immmer就是利用的Proxy的特性实现的。

immer原理

主要理解的是pruduce这个api,大部分情况下使用这个api就已经可以达到实现不可变数据的要求了。

const obj2=produce(obj, draft => {
  draft.count++
})

obj 是个普通对象,immer通过proxy给obj生成了一份草稿draft对象,当你对draft进行操作时,都会被监听,对draft的修改会进入自定义的setter函数。
在setter函数中,它并不会修改原始对象的值,而是递归父级不断拷贝,最终返回新的顶层对象,并作为produce函数的值。

produce

produce 方式其实就是Immer类中的produce方法

    produce(base: any, recipe?: any, patchListener?: any) {
        // curried invocation
        // 若base为函数,则返回一个调用函数curriedProduce,
        // curriedProduce进行正常调用produce,即base为对象,recipe为函数
        if (typeof base === "function" && typeof recipe !== "function") {
            
            const defaultBase = recipe
            recipe = base

            const self = this
            return function curriedProduce(
                this: any,
                base = defaultBase,
                ...args: any[]
            ) {
                return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args)) // prettier-ignore
            }
        }

        //下面是base 为对象,recipe为函数, produce的主流程
        //
        if (typeof recipe !== "function") die(6)
        if (patchListener !== undefined && typeof patchListener !== "function")
            die(7)

        let result

        // Only plain objects, arrays, and "immerable classes" are drafted.
        // 只有base 是对象 ,数组,才会生成草稿immerable classes 

        //判断base 是否能被草稿化
        if (isDraftable(base)) {

            //对currentScope进行初始化
            /**scope的数据格式:
             * {
                    drafts_: [],
                    parent_,
                    immer_,
                    // Whenever the modified draft contains a draft from another scope, we
                    // need to prevent auto-freezing so the unowned draft can be finalized.
                    canAutoFreeze_: true,
                    unfinalizedDrafts_: 0
                }
             */
            const scope = enterScope(this)

            // 获取基于base的代理对象,用于监听处理 后续recipe函数操作并生成不可变对象
            const proxy = createProxy(this, base, undefined)
            let hasError = true
            try {
                //执行函数,一般回调函数不返回数据,所以result一般为undefined
                result = recipe(proxy)
                hasError = false
            } finally {
                // finally instead of catch + rethrow better preserves original stack
                if (hasError) revokeScope(scope)
                else leaveScope(scope)
            }
            if (typeof Promise !== "undefined" && result instanceof Promise) {
                //对于Promise 类型的处理
                return result.then(
                    result => {
                        usePatchesInScope(scope, patchListener)
                        return processResult(result, scope)
                    },
                    error => {
                        revokeScope(scope)
                        throw error
                    }
                )
            }
            usePatchesInScope(scope, patchListener)

            //解析并返回结果数据
            return processResult(result, scope)
        } else if (!base || typeof base !== "object") {
            result = recipe(base)
            if (result === NOTHING) return undefined
            if (result === undefined) result = base
            if (this.autoFreeze_) freeze(result, true)
            return result
        } else die(21, base)
    }
    
    
  • enterScope 生成当前全局的scope,后续用于保存draft,可以调用getCurrentScope来获取全局scope,进入获取draft
  • 调用createProxy,获取基于base的代理,从而生成draft,实现监听和处理相关的数据操作。
  • 调用recipe,实现用户的数据操作,返回result
  • 调用processResult来返回最终修改完成的不可变数据

enterScope

export function enterScope(immer: Immer) {
    //  createScope返回scope,并赋值给全局currentScope
    return (currentScope = createScope(currentScope, immer))
}

function createScope(
    parent_: ImmerScope | undefined,
    immer_: Immer
): ImmerScope {
    //返回 immerscope对象
    return {
        drafts_: [],
        parent_,
        immer_,
        // Whenever the modified draft contains a draft from another scope, we
        // need to prevent auto-freezing so the unowned draft can be finalized.
        // 只要修改后的draft包含来自其他作用域的draft,我们需要在最终draft确定前,防止其自动冻结。
        canAutoFreeze_: true,
        unfinalizedDrafts_: 0
    }
}

createProxy

export function createProxy<T extends Objectish>(
    immer: Immer,
    value: T,
    parent?: ImmerState
): Drafted<T, ImmerState> {
    // precondition: createProxy should be guarded by isDraftable, so we know we can safely draft
    // 前提条件: createProxy 应该有 isDraftable函数判断保护,所以我们才能在草稿进行安全地操作。


    //下面是value ,即传入produce的base的数据类型各自对于的处理
    const draft: Drafted = isMap(value)
        ? getPlugin("MapSet").proxyMap_(value, parent)
        : isSet(value)
        ? getPlugin("MapSet").proxySet_(value, parent)
        : immer.useProxies_  //判断是否支持 ES6的Proxy
        ? createProxyProxy(value, parent)  //** 支持es6 Proxy,生成draft
        : getPlugin("ES5").createES5Proxy_(value, parent)  //不支持,采用自带的es5"Proxy"

    const scope = parent ? parent.scope_ : getCurrentScope()
    scope.drafts_.push(draft)
    return draft
}
export function createProxyProxy<T extends Objectish>(
    base: T,
    parent?: ImmerState
): Drafted<T, ProxyState> {
    const isArray = Array.isArray(base)
    
    //初始化 base 的状态,用于记录后续数据操作的一些变化
    const state: ProxyState = {
        type_: isArray ? ProxyTypeProxyArray : (ProxyTypeProxyObject as any),
        // Track which produce call this is associated with.
        scope_: parent ? parent.scope_ : getCurrentScope()!,
        // True for both shallow and deep changes.
        modified_: false,
        // Used during finalization.
        finalized_: false,
        // Track which properties have been assigned (true) or deleted (false).
        assigned_: {},
        // The parent draft state.
        parent_: parent,
        // The base state.
        base_: base,
        // The base proxy.
        draft_: null as any, // set below
        // The base copy with any updated values.
        copy_: null,
        // Called by the `produce` function.
        revoke_: null as any,
        isManual_: false
    }
    
    let target: T = state as any

    // 默认捕获器 是obj类型的,若base 是数组类型,则捕获器改为对应的数组类型
    let traps: ProxyHandler<object | Array<any>> = objectTraps
    if (isArray) {
        target = [state] as any
        traps = arrayTraps
    }
    
    //生产Proxy代理对象
    const {revoke, proxy} = Proxy.revocable(target, traps)
    state.draft_ = proxy as any
    state.revoke_ = revoke
    return proxy as any
}

processResult

// 解析结果,并返回copy数据 或者 base数据
export function processResult(result: any, scope: ImmerScope) {
    scope.unfinalizedDrafts_ = scope.drafts_.length
    //获取数据的代理对象
    const baseDraft = scope.drafts_![0]
    const isReplaced = result !== undefined && result !== baseDraft
    if (!scope.immer_.useProxies_)
        getPlugin("ES5").willFinalizeES5_(scope, result, isReplaced)
    if (isReplaced) {
        if (baseDraft[DRAFT_STATE].modified_) {
            revokeScope(scope)
            die(4)
        }
        if (isDraftable(result)) {
            // Finalize the result in case it contains (or is) a subset of the draft.
            result = finalize(scope, result)
            if (!scope.parent_) maybeFreeze(scope, result)
        }
        if (scope.patches_) {
            getPlugin("Patches").generateReplacementPatches_(
                baseDraft[DRAFT_STATE],
                result,
                scope.patches_,
                scope.inversePatches_!
            )
        }
    } else {
        // Finalize the base draft.
        // result结果一般为undefined,所以一般为直接调用finalize,这个函数对返回state.copy_
        result = finalize(scope, baseDraft, [])
    }
    revokeScope(scope)
    if (scope.patches_) {
        scope.patchListener_!(scope.patches_, scope.inversePatches_!)
    }
    return result !== NOTHING ? result : undefined
}

总结下

从源码大致可以看出,传入produce的数据value(value为被drafted,主要为引用类型),会通过proxy生成一个代理对象valueProxy和对应的state。
state是每个value对象对应的状态,用于记录数据是否发生变更,是否需要生成新的copy,是否需要进行替换等。
valueProxy可以理解为我们操作的draft,valueProxy通过监听get、set方法等,根据state来判断和处理数据。
最终返回我们处理后的不可变数据。

参考

1.ECMAScript 6 入门
2.immer官网文档
3.精读《Immer.js》源码——黄子毅
4.Immer 全解析——Sheepy

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354