Mobx——computed 装饰器的实现原理 (离职拷贝版)

离职了,把 2019 年在公司写的文档 copy 出来。年头有点久,可能写的不太对,也不是很想改了~
注:本文档对应 mobx 版本为 4.15.4、mobx-vue 版本为 2.0.10

对比 Vue 的猜想

Vue computed 的实现逻辑

  1. initComputed:遍历 option.computed 建立 Watcher, 然后执行 defineComputed
  2. new Watcher(vm, getter || noop, noop, computedWatcherOptions),初始化对应的 watcher,Watcher 的四个参数为 vm 实例、 computed 的行为函数(也就是根据 computed的格式拿到执行函数)、 noop、 和默认的computedWatcherOptions: { lazy: true }
  3. defineComputed(target, key, userDef),对应三个参数为 vm 实例、计算属性的key值、计算属性的对应的执行函数
    a. 定义sharedPropertyDefinition,sharedPropertyDefinition.get = createComputedGetter(key),sharedPropertyDefinition.set = noop;
    b. 其中 createComputedGetter
function createComputedGetter (key) {
    return function computedGetter () {
        var watcher = this._computedWatchers && this._computedWatchers[key];
        if (watcher) {
            if (watcher.dirty) {
                watcher.evaluate();
            }
            if (Dep.target) {
                watcher.depend();
            }
            return watcher.value
        }
    }
}

c. 执行 Object.defineProperty(target, key, sharedPropertyDefinition);

值得注意的是 computed 的 Watcher 传进去的 option 为 lazy: true

这也就意味着在派发更新阶段,虽然你 Watcher 被 notify 了,但是并不会被执行下去,而是在主 Watcher 执行了他自己的 run -> get -> getter -> vm._updata => vm._render 过程中,在最后的 _render 里触发到了 computed 的 getter,这时候才会涉及到 computed 属性的计算并赋一个新值。 用原子更新—— value 和 计算属性 squared 以及 计算属性 cubic 的例子讲解具体流程如下:

  1. 改变 value 的值
  2. Dep.notify 遍历 Dep 下面的 Watcher 数组(相关依赖的 Watcher ), 执行各自的 updata,这个案例中涉及到 Dep 下面的3个 Watcher 分别是,value 对应的 vm 双向绑定的 Watcher、计算属性 squared 的 Watcher、计算属性 cubic 的 Watcher,
  3. updata 就是 lazy 啥也不干,不 lazy 的如果是同步则执行 run / 异步走 queueWatcher 攒一波然后再 run,run 就是执行 get,get 就是执行 getter,这里因为 computed 是 lazy 所以只走了主 Watcher,来执行 vm._update。那些 lazy 的 getter 就是你定义计算属性传进去的那个函数,然后把返回值付给 Wathcer 的 value,只是一个赋值过程,
  4. 最后在 render 过程中,需要使用 computed 的变量,触发到 computed 的 get,因为在 Watcher.updata 的时候 dirty 被置成了 true,所以在上述 3.b 中执行 evaluate ,evaluate 干了两件事:this.value = this.getter(); this.dirty = false,然后返回 watcher.value,即计算后的平方和立方

Vue watch 的实现逻辑

其实和 mobx 的 @computed 没啥联系,但在 Vue 里 computed 和 watch 还是有比较通用的使用场景的,顺便追下源码,看看 watch 和 computed 的区别。

还是上面那个平方和立方的例子,如果用 watch 来做,就需要额外定义两个 data,然后 watch 了之后改变其对应的值,这么写的话,会产生额外 2 个 Dep,也就是 squared 的依赖项和 cubic 的依赖项,源码差不多都是一个意思,都得 new 一个 Watcher,只不过响应式的实现从计算属性的渲染时计算 变成了 data 的双向绑定,先改变值再渲染,具体流程如下:

  1. 改变 value 的值
  2. Dep.notify 遍历 Dep 下面的 Watcher 数组(相关依赖的 Watcher ), 执行各自的 updata,这里面会有 3 个 Dep.notify,因为值、平方、立方都变了,各自的 Dep 下面都有渲染视图的那个主 Watcher,不过 Watcher 有去重功能
  3. 然后就是执行平方的 Watcher、 立方的 Watcher、 主 Watcher
  4. 最后再 render

如果你好奇 watch 生成的 Wachter 是怎么和 Dep 建立关系的,可以看下 Watcher 里 getter 的定义:

this.getter = parsePath(expOrFn); // 这里expOrFn 就是你定义的回调函数

function parsePath (path) {
    if (bailRE.test(path)) {
        return
    }
    var segments = path.split('.');
    return function (obj) {
        for (var i = 0; i < segments.length; i++) {
            if (!obj) { return }
                obj = obj[segments[i]];
        }
        return obj
    }
}

在首次依赖收集的时候 Watcher.get 会执行 value = this.getter.call(vm, vm);这时候 vm 被扔进去,正好触发了依赖项 data.value 的 get,然后 dep 和 Watcher 的依赖关系被建立

源码解析

其实这里和 @observable 会执行几乎一样的装饰器逻辑。少一层 createDecoratorForEnhancer,最后执行的是 addComputedProp,而不是addObservableProp

  1. createPropDecorator
const computed: IComputed = function computed(arg1, arg2, arg3) {
    if (typeof arg2 === "string") {
        // @computed
        return computedDecorator.apply(null, arguments)
    }
    ...
}


const computedDecorator = createPropDecorator(
    false,
    (
        instance: any,
        propertyName: PropertyKey,
        descriptor: any,
        decoratorTarget: any,
        decoratorArgs: any[]
    ) => {
        const { get, set } = descriptor
        const options = decoratorArgs[0] || {}
        // 4版本如下
        defineComputedProperty(instance, propertyName, { get, set, ...options })
        
        // 对应5版本的代码比较直接
        asObservableObject(instance).addComputedProp(instance, propertyName, {
            get,
            set,
            context: instance,
            ...options
        })
    }
)

省去中间相同的逻辑,直接看 defineComputedProperty

  1. defineComputedProperty
function defineComputedProperty(
    target: any,
    propName: string,
    options: IComputedValueOptions<any>
) {
    const adm = asObservableObject(target)
    options.name = `${adm.name}.${propName}`
    options.context = target
    adm.values[propName] = new ComputedValue(options)
    Object.defineProperty(target, propName, generateComputedPropConfig(propName))
}

和上面稍微有些不同,但是核心逻辑都一样,拿到 ObservableObjectAdministration,然后往 value 上面挂东西,只不过这里挂的是 ComputedValue 而不是 ObservableValue

  1. generateComputedPropConfig
function generateObservablePropConfig(propName) {
    return (
        observablePropertyConfigs[propName] ||
        (observablePropertyConfigs[propName] = {
            configurable: true,
            enumerable: true,
            get() {
                return this.$mobx.read(this, propName)
            },
            set(v) {
                this.$mobx.write(this, propName, v)
            }
        })
    )
}

结论就是和 @observable 差不多的实现流程,也就是想办法搞一套 get 和 set 逻辑,但是之所以把 Vue 的 computed 和 watch 拉出来讲一下,是为了突出 Vue 里面 computed 创建的 Watcher 和一般的 Wathcer 不太一样,派发更新的时机和方法也独树一帜。反观 Mobx 的@computed,不仅加不加都行,而且专门弄出来一个和 Reaction(对应 Vue 的 Watcher) 平级的 ComputedValue,意义就是在 @observable 依赖项发生变化时,不仅仅要触发更新视图的 Reaction,还要对监听自己的 ComputedValue 进行更新,触发的顺序也是在 Vue 的 render 之后,但是在 Vue + 全局 mobx 的环境下有些显得多余。

举个例子就是:在我们的项目里如果有一个组件 A 自己维护 @observable valueA,然后他有一个子组件 B, 用 props 传递了 valueA,并让组件 B 的 VM @computed 了一下 valueA,@observable valueA 上只有更新组件 A 的 Reaction,所以 valueA 变化之后组件 B 并不会更新。

但是项目中并没有这样的操作,变量都维护在了全局的 Store 中,每个 Vue 都会有 extends BaseVM 的 VM,所以一旦 @observable 发生变动,所有的组件都会被重新渲染

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

推荐阅读更多精彩内容