Mobx——computed 裝飾器的實現(xiàn)原理 (離職拷貝版)

離職了,把 2019 年在公司寫的文檔 copy 出來。年頭有點久哮独,可能寫的不太對,也不是很想改了~
注:本文檔對應 mobx 版本為 4.15.4统诺、mobx-vue 版本為 2.0.10

對比 Vue 的猜想

Vue computed 的實現(xiàn)邏輯

  1. initComputed:遍歷 option.computed 建立 Watcher, 然后執(zhí)行 defineComputed
  2. new Watcher(vm, getter || noop, noop, computedWatcherOptions),初始化對應的 watcher疑俭,Watcher 的四個參數(shù)為 vm 實例粮呢、 computed 的行為函數(shù)(也就是根據 computed的格式拿到執(zhí)行函數(shù))、 noop、 和默認的computedWatcherOptions: { lazy: true }
  3. defineComputed(target, key, userDef)啄寡,對應三個參數(shù)為 vm 實例豪硅、計算屬性的key值、計算屬性的對應的執(zhí)行函數(shù)
    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. 執(zhí)行 Object.defineProperty(target, key, sharedPropertyDefinition);

值得注意的是 computed 的 Watcher 傳進去的 option 為 lazy: true

這也就意味著在派發(fā)更新階段,雖然你 Watcher 被 notify 了姻乓,但是并不會被執(zhí)行下去嵌溢,而是在主 Watcher 執(zhí)行了他自己的 run -> get -> getter -> vm._updata => vm._render 過程中,在最后的 _render 里觸發(fā)到了 computed 的 getter蹋岩,這時候才會涉及到 computed 屬性的計算并賦一個新值。 用原子更新—— value 和 計算屬性 squared 以及 計算屬性 cubic 的例子講解具體流程如下:

  1. 改變 value 的值
  2. Dep.notify 遍歷 Dep 下面的 Watcher 數(shù)組(相關依賴的 Watcher )学少, 執(zhí)行各自的 updata剪个,這個案例中涉及到 Dep 下面的3個 Watcher 分別是,value 對應的 vm 雙向綁定的 Watcher版确、計算屬性 squared 的 Watcher扣囊、計算屬性 cubic 的 Watcher,
  3. updata 就是 lazy 啥也不干绒疗,不 lazy 的如果是同步則執(zhí)行 run / 異步走 queueWatcher 攢一波然后再 run侵歇,run 就是執(zhí)行 get,get 就是執(zhí)行 getter吓蘑,這里因為 computed 是 lazy 所以只走了主 Watcher惕虑,來執(zhí)行 vm._update。那些 lazy 的 getter 就是你定義計算屬性傳進去的那個函數(shù)磨镶,然后把返回值付給 Wathcer 的 value溃蔫,只是一個賦值過程,
  4. 最后在 render 過程中琳猫,需要使用 computed 的變量伟叛,觸發(fā)到 computed 的 get,因為在 Watcher.updata 的時候 dirty 被置成了 true脐嫂,所以在上述 3.b 中執(zhí)行 evaluate 统刮,evaluate 干了兩件事:this.value = this.getter(); this.dirty = false,然后返回 watcher.value账千,即計算后的平方和立方

Vue watch 的實現(xiàn)邏輯

其實和 mobx 的 @computed 沒啥聯(lián)系侥蒙,但在 Vue 里 computed 和 watch 還是有比較通用的使用場景的,順便追下源碼蕊爵,看看 watch 和 computed 的區(qū)別辉哥。

還是上面那個平方和立方的例子,如果用 watch 來做,就需要額外定義兩個 data醋旦,然后 watch 了之后改變其對應的值恒水,這么寫的話,會產生額外 2 個 Dep饲齐,也就是 squared 的依賴項和 cubic 的依賴項钉凌,源碼差不多都是一個意思,都得 new 一個 Watcher捂人,只不過響應式的實現(xiàn)從計算屬性的渲染時計算 變成了 data 的雙向綁定御雕,先改變值再渲染,具體流程如下:

  1. 改變 value 的值
  2. Dep.notify 遍歷 Dep 下面的 Watcher 數(shù)組(相關依賴的 Watcher )滥搭, 執(zhí)行各自的 updata酸纲,這里面會有 3 個 Dep.notify,因為值瑟匆、平方闽坡、立方都變了愁溜,各自的 Dep 下面都有渲染視圖的那個主 Watcher疾嗅,不過 Watcher 有去重功能
  3. 然后就是執(zhí)行平方的 Watcher、 立方的 Watcher冕象、 主 Watcher
  4. 最后再 render

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

this.getter = parsePath(expOrFn); // 這里expOrFn 就是你定義的回調函數(shù)

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 會執(zhí)行 value = this.getter.call(vm, vm);這時候 vm 被扔進去,正好觸發(fā)了依賴項 data.value 的 get渐扮,然后 dep 和 Watcher 的依賴關系被建立

源碼解析

其實這里和 @observable 會執(zhí)行幾乎一樣的裝飾器邏輯论悴。少一層 createDecoratorForEnhancer,最后執(zhí)行的是 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 差不多的實現(xiàn)流程齐饮,也就是想辦法搞一套 get 和 set 邏輯捐寥,但是之所以把 Vue 的 computed 和 watch 拉出來講一下,是為了突出 Vue 里面 computed 創(chuàng)建的 Watcher 和一般的 Wathcer 不太一樣祖驱,派發(fā)更新的時機和方法也獨樹一幟握恳。反觀 Mobx 的@computed,不僅加不加都行捺僻,而且專門弄出來一個和 Reaction(對應 Vue 的 Watcher) 平級的 ComputedValue乡洼,意義就是在 @observable 依賴項發(fā)生變化時崇裁,不僅僅要觸發(fā)更新視圖的 Reaction,還要對監(jiān)聽自己的 ComputedValue 進行更新束昵,觸發(fā)的順序也是在 Vue 的 render 之后拔稳,但是在 Vue + 全局 mobx 的環(huán)境下有些顯得多余。

舉個例子就是:在我們的項目里如果有一個組件 A 自己維護 @observable valueA锹雏,然后他有一個子組件 B巴比, 用 props 傳遞了 valueA,并讓組件 B 的 VM @computed 了一下 valueA礁遵,@observable valueA 上只有更新組件 A 的 Reaction轻绞,所以 valueA 變化之后組件 B 并不會更新。

但是項目中并沒有這樣的操作佣耐,變量都維護在了全局的 Store 中政勃,每個 Vue 都會有 extends BaseVM 的 VM,所以一旦 @observable 發(fā)生變動兼砖,所有的組件都會被重新渲染

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末稼病,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子掖鱼,更是在濱河造成了極大的恐慌,老刑警劉巖援制,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件戏挡,死亡現(xiàn)場離奇詭異,居然都是意外死亡晨仑,警方通過查閱死者的電腦和手機褐墅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洪己,“玉大人妥凳,你說我怎么就攤上這事〈鸩叮” “怎么了逝钥?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拱镐。 經常有香客問我艘款,道長,這世上最難降的妖魔是什么沃琅? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任哗咆,我火速辦了婚禮,結果婚禮上益眉,老公的妹妹穿的比我還像新娘晌柬。我一直安慰自己姥份,他們只是感情好,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布年碘。 她就那樣靜靜地躺著澈歉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盛泡。 梳的紋絲不亂的頭發(fā)上闷祥,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機與錄音傲诵,去河邊找鬼凯砍。 笑死,一個胖子當著我的面吹牛拴竹,可吹牛的內容都是我干的悟衩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼栓拜,長吁一口氣:“原來是場噩夢啊……” “哼座泳!你這毒婦竟也來了?” 一聲冷哼從身側響起幕与,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤挑势,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后啦鸣,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體潮饱,經...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年诫给,在試婚紗的時候發(fā)現(xiàn)自己被綠了香拉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡中狂,死狀恐怖凫碌,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情胃榕,我是刑警寧澤盛险,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站勤晚,受9級特大地震影響枉层,放射性物質發(fā)生泄漏。R本人自食惡果不足惜赐写,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一鸟蜡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧挺邀,春花似錦揉忘、人聲如沸跳座。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疲眷。三九已至,卻和暖如春您朽,著一層夾襖步出監(jiān)牢的瞬間狂丝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工哗总, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留几颜,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓讯屈,卻偏偏與公主長得像蛋哭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子涮母,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內容