Mobx 原始碼與設計思想

Sadhu發表於2022-04-06

Proxy

攔截方式

Mobx 暴露的攔截的 API 有多種,概括來說可以分為裝飾器式和基於 observable 方法呼叫。

裝飾器
對裝飾器不太明白的同學,可以見我以往一篇文章:裝飾器原理探究 ,通過分析轉譯後的 ES 程式碼得出裝飾器的行為。

由於裝飾器在 ES 裡還處於提案中且各階段的裝飾器行為不一致,故 mobx 6.x 起就淘汰了裝飾器的寫法(也可以手動開啟),本文的原始碼分析基於 mobx 5.x 版本(所述原理與 6.x 一模一樣),此時裝飾器基於 babel

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
  ]
}

該配置下實現,使用歷史遺留(stage 1)的裝飾器中的語法和行為。

import { observable } from 'mobx';

class A {
  @observable a = 1;
}

此處 @observable 裝飾器的行為其實就是在例項化前往 A 的原型上掛 getter setter。

{
            configurable: true,
            enumerable: enumerable,
            get: function() {
                initializeInstance(this)
                return this[prop]
            },
            set: function(value) {
                initializeInstance(this)
                this[prop] = value
            }
        }

在例項化時會執行 instance.a = 1 賦值操作,觸發 setter ,走到 mobx 處理類例項的邏輯:

  1. 往例項上掛 物件管理類(adm)
  2. 遞迴包裝 value , 並收集在 adm
  3. 為例項上的 key (a) 掛 getter setter
{
            configurable: true,
            enumerable: true,
            get() {
                // 收集依賴
                return this[$mobx].read(propName)
            },
            set(v) {
                // 觸發更新
                this[$mobx].write(propName, v)
            }
}

巨集觀來講,此後訪問裝飾的屬性就會走到 this[$mobx].read(propName) 收集副作用,當屬性改變就走到 this[$mobx].write(propName, v) 執行副作用。

observable 命令式呼叫

命令式呼叫就是如下這種:

const xxx = observable(xxx) || observable.xx(xxx);

一定會有一個返回值,當我們操作返回值的時候,就會做收集 | 執行副作用的行為。

下面會挨個解析各個型別的攔截情況。

先說明個概念,在 Mobx裡,所有需要被觀察的 value ,除了陣列、Set,都會被 ObservableValue 類包裝(為了方便之後對其例項簡稱 OV ),做的工作就是:

(1)使用 enhancer 處理 value

(2)管理(1)中包裝後的 value (讀寫、收集依賴等)

enhancer 有多種,若使用者不作額外配置,Mobx 裡預設對每個 value 使用 deepEnhancer 進行包裝,其實就是遞迴對這個 value 做 observable 命令式呼叫 的操作。

primitive value

對於原始型別 value ,Mobx 裡只支援使用 observable.box(val) 這個 API 進行攔截,其實內部就是返回了個 OV。

如果讀寫分別用 OV 暴露 get set API。

object

使用 observable.object(val) 進行攔截,內部做了三件事:

  1. 新建一個空物件 { }, 並給 { } 掛上 物件管理類(adm)
  2. Proxy 攔截 { },並把代理物件儲存在 adm.proxy
  3. 遍歷 val 的 keys:

    1. 遞迴包裝 value , 並收集 OV 在 adm
    2. 在 { } 掛上每個 key 的 getter setter (同裝飾器掛的 getter setter 一樣)
  4. 返回代理物件

Proxy 的 handlers 有:

// 暫時只關注讀寫
{
    get(target: IIsObservableObject, name: PropertyKey) {
        // ... 忽略暫時無關程式碼
        const adm = getAdm(target)
        // 拿到 OV
        const observable = adm.values.get(name)
        if (observable instanceof Atom) {
              // 此處等同於呼叫 adm.read(propName)
            const result = (observable as any).get() 
            // ...
            return result
        }
        // ... 
    },
    set(target: IIsObservableObject, name: PropertyKey, value: any) {
        if (!isPropertyKey(name)) return false
        set(target, name, value)
          // 這個 set 方法針對物件最終執行如下
          // // ...
        // if (isObservableObject(obj)) {
        //  const adm = ((obj as any) as IIsObservableObject)[$mobx]
        //  // 拿到 OV
        //  const existingObservable = adm.values.get(key)
        //  if (existingObservable) {
        //      adm.write(key, value)
        //  }
        //  // ...
          // }
        // //...
        return true
    },
}

由上可知,此處的讀取處理最終也是和裝飾器方式修飾的物件屬性的讀寫處理相同。

array

使用 observable.array(val) 進行攔截,內部做了五件事:

  1. 初始化 陣列管理類 (adm) ,掛載在 [ ] 上,再把 [ ] 掛載在 adm.values
  2. Proxy 攔截 [ ],並把代理物件掛載在 adm.proxy
  3. 遍歷 val,遞迴包裝每個元素
  4. 更新 3 中一個個 OV 在 adm.values 裡
  5. 返回代理物件

Handler 如下:

get(target, name) {
        if (name === $mobx) return target[$mobx]
        if (name === "length") return target[$mobx].getArrayLength()
        if (typeof name === "number") {
            return arrayExtensions.get.call(target, name)
        }
        if (typeof name === "string" && !isNaN(name as any)) {
              
            return arrayExtensions.get.call(target, parseInt(name))
        }
        if (arrayExtensions.hasOwnProperty(name)) {
            // arrayExtensions 捕獲陣列方法
            return arrayExtensions[name]
        }
        return target[name]
    },
set(target, name, value): boolean {
        if (name === "length") {
            target[$mobx].setArrayLength(value)
        }
        if (typeof name === "number") {
            arrayExtensions.set.call(target, name, value)
        }
        if (typeof name === "symbol" || isNaN(name)) {
            target[name] = value
        } else {
            // numeric string
            arrayExtensions.set.call(target, parseInt(name), value)
        }
        return true
    },

以讀取為例說明,在 arrayExtensions 裡是這樣的:

get(index: number): any | undefined {
            const adm: ObservableArrayAdministration = this[$mobx]
            if (adm) {
                if (index < adm.values.length) {
                    adm.atom.reportObserved()
                    return adm.dehanceValue(adm.values[index])
                }
                // ...
            }
            return undefined
        },

        set(index: number, newValue: any) {
            const adm: ObservableArrayAdministration = this[$mobx]
            const values = adm.values
            if (index < values.length) {
                // update at index in range
                checkIfStateModificationsAreAllowed(adm.atom)
                const oldValue = values[index]
                // ...
                // 新的被 enhancer 包裝過的 value 
                newValue = adm.enhancer(newValue, oldValue)
                const changed = newValue !== oldValue
                if (changed) {
                    values[index] = newValue // 改變 adm 裡收集的舊 value
                    // 通知更新
                    adm.notifyArrayChildUpdate(index, newValue, oldValue)
                }
            } 
              // ...
        }

之前說過,陣列不會被 ObservableValue 包裝,因為在其管理類裡面,已經實現了 ObservableValue 的工作,也就是:

(1)使用 enhancer 處理 value

(2)管理(1)中包裝後的 value (讀寫、收集依賴等)

其實, arrayExtensions 裡的操作,核心也是收集依賴和觸發更新。

map

使用 observable.map(val) 進行攔截,內部做了三件事:

  1. 初始化 map 管理類
  2. 遍歷 val ,挨個 ObservableValue 包裝 value,收集在管理類的 this._data
  3. 返回 map 管理類例項

返回的例項,有 Map 的 API 方法,以讀寫為例:

get(key: K): V | undefined {
              // this._data.get(key)!.get() 等同於呼叫物件 adm 的 adm.read(propName)
              // 收集依賴
        if (this.has(key)) return this.dehanceValue(this._data.get(key)!.get())
        return this.dehanceValue(undefined)
    }

set(key: K, value: V) {
        const hasKey = this._has(key)
        // ...
        if (hasKey) {
            this._updateValue(key, value)
        } else {
            this._addValue(key, value)
        }
        return this
    }

_updateValue(key: K, newValue: V | undefined) {
              // 拿到 ObservableV
        const observable = this._data.get(key)!
        // enhancer 新 value,然後對比舊 value 是否相等
        newValue = (observable as any).prepareNewValue(newValue) as V
        if (newValue !== globalState.UNCHANGED) {
            // ...
            // 更新並通知更新
            observable.setNewValue(newValue as V)
            // ...
        }
    }

set

使用 observable.set(val) 進行攔截,內部做了三件事:

  1. 初始化 set 管理類
  2. 遍歷 val ,挨個 enhancer 包裝 value,收集在管理類的 this._data
  3. 返回 set 管理類例項

和 Map 一樣,返回的 set 管理類也有 Set 的相關 API,以獲取所有 values 和 add 為例:

add(value: T) {
        // ... 
        if (!this.has(value)) {
            transaction(() => {
                // 往本地快取的 _data 裡新增 enhancer 後的 value
                this._data.add(this.enhancer(value, undefined))
                // 通知依賴更新
                this._atom.reportChanged()
            })
            // ...
        }

        return this
    }

keys(): IterableIterator<T> {
        return this.values()
    }

values(): IterableIterator<T> {
        // 通知收集依賴
        this._atom.reportObserved()
        const self = this
        let nextIndex = 0
        const observableValues = Array.from(this._data.values())
        // 在 for of 中挨個讀 _data 的值
        return makeIterable<T>({
            next() {
                return nextIndex < observableValues.length
                    ? { value: self.dehanceValue(observableValues[nextIndex++]), done: false }
                    : { done: true }
            }
        } as any)
    }

由上述可知,其實就是對於不同的資料結構,處理的核心的就是攔截被觀察者 getter setter 或相關 API,達到在讀取時收集依賴,變化時通知依賴更新的目的。

Derivation

Mobx 裡有派生的概念,類似於觀察者。在 Derivation 內使用了 Proxy 的產物,每當產物有變化時則派生(通知)了 Derivation(觀察者)。

一些概念:

transaction

引用了資料庫事務的概念,Mobx 中的事務用於批量處理 Reaction(Derivation 管理者) 的執行,避免不必要的重新計算。Mobx 的事務實現比較簡單,使用 startBatch 和 endBatch 來開始和結束一個事務:

function startBatch() {
  // 通過一個全域性的變數 inBatch 標識事務巢狀的層級
  globalState.inBatch++
}

function endBatch() {
  // 最外層事務結束時,才開始執行重新計算
  if (--globalState.inBatch === 0) {
    // 執行所有 Reaction
    runReactions()
    // 處理不再被觀察的 ObservableV
    const list = globalState.pendingUnobservations
    for (let i = 0; i < list.length; i++) {
      const observable = list[i]
      observable.isPendingUnobservation = false
      if (observable.observers.length === 0) {
          observable.onBecomeUnobserved()
      }
    }
    globalState.pendingUnobservations = []
  }
}

例如,一個 Action 開始和結束時同時伴隨著事務的啟動和結束,確保 Action 中(可能多次)對狀態的修改只觸發一次 Reaction 的重新執行。

function startAction() {
  // ...
  startBatch()
  // ...
}
function endAction() {
  // ...
  endBatch()
  // ...
}
Reaction

Reaction 就是 Derivation 的管理者,實現了 Derivation 的介面:

interface IDerivation extends IDepTreeNode {
  // 依賴陣列
  observing: IObservable[]
  // 每次執行收集到的新依賴陣列
  newObserving: null | IObservable[]
  // 依賴的狀態
  dependenciesState: IDerivationState
  // 每次執行都會有一個 uuid,配合 Observable 的 lastAccessedBy 屬性做簡單的效能優化
  runId: number
  // 執行時新收集的未繫結依賴數量
  unboundDepsCount: number
  // 依賴過期時執行
  onBecomeStale()
}
Derivation 狀態機

Derivation 通過 dependenciesState 屬性標記依賴的四種狀態:

  1. NOT_TRACKING:在執行之前,或事務之外,或未被觀察(計算值)時,所處的狀態。此時 Derivation 沒有任何關於依賴樹的資訊。列舉值-1
  2. UP_TO_DATE:表示所有依賴都是最新的,這種狀態下不會重新計算。列舉值0
  3. POSSIBLY_STALE:計算值才有的狀態,表示深依賴發生了變化,但不能確定淺依賴是否變化,在重新計算之前會檢查。列舉值1
  4. STALE:過期狀態,即淺依賴發生了變化,Derivation 需要重新計算。列舉值2

任何狀態都趨於 UP_TO_DATE。

------------------------- 2 ------------------------- STALE

-------------↓--- 1 ------------------ POSSIBLY_STALE

↓        ↓

------- 0 -------- UP_TO_DATE

-1--- NOT_TRACKING

狀態機的規律是:

  1. 初始都是 NOT_TRACKING,繫結起依賴和派生關係後集體變為 U_T_D。
    解綁則回退為 NOT_TRACKING。
  2. 某收集的依賴發生變化時,其自身依賴狀態和 Derivation (onBecomeStale後)都變為 STALE。
    在 Derivation 重新處理後,其自身和收集的依賴都變為 U_T_D。
  3. 計算屬性計算後(含第一次),自身派生狀態、收集的依賴狀態都變為 U_T_D。(符合 2 第二句 Derivation 重新處理後,其自身和收集的依賴都變為 U_T_D)在第一次被繫結後,符合 1。

    若計算屬性收集的某依賴 A 狀態發生變化時,將 A 狀態和 計算屬性派生狀態(onBecomeStale後) 為 STALE(符合 2 第一句),並且把 計算屬性依賴狀態、計算屬性派生的 Derivation 置為 P_STALE(區別)。在計算屬性重新計算後自身派生狀態、收集的所有依賴狀態變更為 U_T_D(符合 2 第二句),若計算結果無變更,把計算屬性依賴狀態、計算屬性派生的 Derivation 變回 U_T_D 。若有變更,則把 計算屬性派生的 Derivation 變為 STALE,接著重新處理 計算屬性派生的 Derivation,把其和其收集的依賴(含計算屬性作為依賴)狀態 變為 U_P_D。

下面以 AutoRun、Computed Value 、React Render 為例分析 Derivation 的原始碼。

AutoRun

流程

常規用法是:

autorun(cb)

首先,會為 AutoRun 這個 Derivation 初始化一個 Reaction 用於管理:

function autorun(
    view: (r: IReactionPublic) => any, // cb
    opts: IAutorunOptions = EMPTY_OBJECT // 忽略
): IReactionDisposer {

    const name: string = (opts && opts.name) || (view as any).name || "Autorun@" + getNextId()
    const runSync = !opts.scheduler && !opts.delay
    let reaction: Reaction

    if (runSync) {
        // normal autorun
        // 用一個 reaction 來管理該 autorun
        reaction = new Reaction(
            name,
            function(this: Reaction) {
                this.track(reactionRunner)
            },
            opts.onError,
            opts.requiresObservable
        )
    }
    function reactionRunner() {
        view(reaction)
    }
        // 將該 reaction 列入計劃表
    reaction.schedule()
    // 返回銷燬方法
    return reaction.getDisposer()
}

計劃表維護了一個全域性的陣列,裡面存的 Reactions 就是該 batch(批次) 中需要執行的 Reaction。

schedule() {
        // Reaction 已經在重新計算的計劃表內,直接返回
        if (!this._isScheduled) {
            this._isScheduled = true
            // 該 Reaction 加入全域性的待重新計算陣列中
            globalState.pendingReactions.push(this)
            runReactions()
        }
    }
export function runReactions() {
    // 惰性更新,若此時處於事務中,inBatch > 0,會直接返回
    if (globalState.inBatch > 0 || globalState.isRunningReactions) return
    reactionScheduler(runReactionsHelper)
}
function runReactionsHelper() {
    globalState.isRunningReactions = true
    // 取出當前批次收集的所有 Reaction
    const allReactions = globalState.pendingReactions
    let iterations = 0

    // 當執行 Reaction 時,可能觸發新的 Reaction(Reaction 內允許設定 Observable的值),加入到 pendingReactions 中
    while (allReactions.length > 0) {
        // 設定 Reaction 計算的最大迭代次數,避免造成死迴圈
        if (++iterations === MAX_REACTION_ITERATIONS) {
            // ... error
            allReactions.splice(0) // clear reactions
        }
        let remainingReactions = allReactions.splice(0)
        for (let i = 0, l = remainingReactions.length; i < l; i++)
            remainingReactions[i].runReaction()
    }
    globalState.isRunningReactions = false
}

接下來就是執行 Reaction 的邏輯了,主要目的是執行 cb ,收集用到的 OV。

runReaction() {
        if (!this.isDisposed) {
            // 開啟一個事務處理,因為執行 cb 的過程中可能會再加 Reaction 到計劃表(比如依賴更新)
            startBatch()
            this._isScheduled = false
            // 判斷 Reaction 收集的依賴狀態
            // 如狀態機所示,只有在 NO_TRACKING | STALE | 判斷 COMPUTED 值變化時才會執行 Reaction 
            if (shouldCompute(this)) {
                this._isTrackPending = true

                try {
                      // 處理 cb 
                    this.onInvalidate()
                    // ...
                } catch (e) {
                    this.reportExceptionInDerivation(e)
                }
            }
            endBatch()
        }
    }

this.onInvalidate 這裡就開始處理 cb 了,核心邏輯是:

function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
    // ...
    // 把 Reaction 和之前收集的被觀察者狀態都置為 UP_TO_DATE
    changeDependenciesStateTo0(derivation)
    derivation.newObserving = new Array(derivation.observing.length + 100)
    // 記錄新的依賴的數量
    derivation.unboundDepsCount = 0
    // 每次執行都分配一個 uid
    derivation.runId = ++globalState.runId
    // 當前 Derivation 記錄到全域性的 trackingDerivation 中,這樣被觀察的 Observable 在其 reportObserved 方法中就能獲取到該 Derivation
    const prevTracking = globalState.trackingDerivation
    globalState.trackingDerivation = derivation
    let result
    if (globalState.disableErrorBoundaries === true) {
        // debug 環境不 catch 異常,若出錯堆疊清晰
        result = f.call(context)
    } else {
        try {
            // 執行響應函式 cb ,收集使用到的所有依賴,加入 newObserving 陣列中
            result = f.call(context)
        } catch (e) {
            result = new CaughtException(e)
        }
    }
    globalState.trackingDerivation = prevTracking
    // 比較新舊依賴,更新依賴
    bindDependencies(derivation)
    // 如果配置了 requiresObservable 但是 cb 內沒引用 OV 的話,報警告
    warnAboutDerivationWithoutDependencies(derivation)
        // ...
}
getter 裡幹了啥?(追蹤依賴)

執行 cb 的時候,讀取到 observable 的值,以裝飾器修飾方式為例,會走到:

read(key: PropertyKey) {
        return this.values.get(key)!.get()
}

this.values.get(key) 拿到的就是 OV,OV 的 get:

public get(): T {
        this.reportObserved()
        return this.dehanceValue(this.value)
}

function reportObserved(observable: IObservable): boolean {
    // ...
    const derivation = globalState.trackingDerivation
    if (derivation !== null) {
        // 避免重複收集 OV 
        if (derivation.runId !== observable.lastAccessedBy) {
            observable.lastAccessedBy = derivation.runId
            derivation.newObserving![derivation.unboundDepsCount++] = observable
            if (!observable.isBeingObserved) {
                observable.isBeingObserved = true
                observable.onBecomeObserved() // 觸發監聽鉤子
            }
        }
        return true
    } else if (observable.observers.size === 0 && globalState.inBatch > 0) {
        // 如果 OV 沒有 derivation 觀察了,準備清除 Observable 
        queueForUnobservation(observable)
    }

    return false
}

其實就是把 OV 收集在 Reaction 的 newObserving 上,至此追蹤依賴就結束了。

處理依賴

接著就是處理收集到的依賴:

  1. 替換 Derivation 的依賴陣列為新收集的依賴
  2. 找出新舊依賴陣列不相交的元素,解綁舊依賴陣列中不相交的 OV 與該 Derivation 的關係(OV 不再收集 Derivation),繫結新依賴陣列中不相交的 OV 與該 Derivation 的關係
function bindDependencies(derivation: IDerivation) {
    // invariant(derivation.dependenciesState !== IDerivationState.NOT_TRACKING, "INTERNAL ERROR bindDependencies expects derivation.dependenciesState !== -1");
    const prevObserving = derivation.observing
    const observing = (derivation.observing = derivation.newObserving!)
    // 記錄更新依賴過程中,新觀察的 Derivation 的最新狀態
    let lowestNewObservingDerivationState = IDerivationState.UP_TO_DATE

    // Go through all new observables and check diffValue: (this list can contain duplicates):
    //   0: first occurrence, change to 1 and keep it
    //   1: extra occurrence, drop it
    // 遍歷新的 observing 陣列,使用 diffValue 這個屬性來輔助 diff 過程:
    // 所有 Observable 的 diffValue 初值都是0(要麼剛被建立,繼承自 BaseAtom 的初值0;
    // 要麼經過上次的 bindDependencies 後,置為了0)
    // 如果 diffValue 為0,保留該 Observable,並將 diffValue 置為1
    // 如果 diffValue 為1,說明是重複的依賴,無視掉
    let i0 = 0,
        l = derivation.unboundDepsCount // 新收集的 ObservableValue 數量
    for (let i = 0; i < l; i++) {
        const dep = observing[i]
        if (dep.diffValue === 0) {
            // 這次此次 Reaction 最新收集的依賴
            dep.diffValue = 1
            // i0 不等於 i,即前面有重複的 dep 被無視,依次往前移覆蓋
            if (i0 !== i) observing[i0] = dep
            i0++
        }

        // Upcast is 'safe' here, because if dep is IObservable, `dependenciesState` will be undefined,
        // not hitting the condition
        if (((dep as any) as IDerivation).dependenciesState > lowestNewObservingDerivationState) {
            lowestNewObservingDerivationState = ((dep as any) as IDerivation).dependenciesState
        }
    }
    observing.length = i0 // 只保留最新一次追蹤 Reaction 收集的依賴

    derivation.newObserving = null // newObserving shouldn't be needed outside tracking (statement moved down to work around FF bug, see #614)

    // Go through all old observables and check diffValue: (it is unique after last bindDependencies)
    //   0: it's not in new observables, unobserve it
    //   1: it keeps being observed, don't want to notify it. change to 0
    // 遍歷 prevObserving 陣列,檢查 diffValue:(經過上一次的 bindDependencies  後,該陣列中不會有重複)
    // 如果為 0,說明沒有在 newObserving 中出現,呼叫 removeObserver 將 dep 和 derivation 間的聯絡移除
    // 如果為 1,依然被觀察,將 diffValue 置為0(在下面的迴圈有用處)
    l = prevObserving.length
    while (l--) {
        const dep = prevObserving[l]
        if (dep.diffValue === 0) {
            removeObserver(dep, derivation)
        }
        dep.diffValue = 0
    }

    // Go through all new observables and check diffValue: (now it should be unique)
    //   0: it was set to 0 in last loop. don't need to do anything.
    //   1: it wasn't observed, let's observe it. set back to 0
    // 再次遍歷新的 observing 陣列,檢查 diffValue
    // 如果為0,說明是在上面的迴圈中置為了0,即是本來就被觀察的依賴,什麼都不做
    // 如果為1,說明是新增的依賴,呼叫 addObserver 新增依賴,並將 diffValue 置為0,為下一次 bindDependencies 做準備
    while (i0--) {
        const dep = observing[i0]
        if (dep.diffValue === 1) {
            dep.diffValue = 0
            addObserver(dep, derivation)
        }
    }

    // Some new observed derivations may become stale during this derivation computation
    // so they have had no chance to propagate staleness (#916)
    // 某些新觀察的 Derivation 可能在依賴更新過程中過期
    // 避免這些 Derivation 沒有機會傳播過期的資訊(#916)
    if (lowestNewObservingDerivationState !== IDerivationState.UP_TO_DATE) {
        derivation.dependenciesState = lowestNewObservingDerivationState
        derivation.onBecomeStale()
    }
}

上面用了 diffValue 標誌位,降低樸素演算法的時間複雜度為線性,給個例子吧:

const a = {};
const b = {};
const c = {};

const prev = [a, b];
const curr = [b, c];

// 找出不相交的 a, c 並做一些處理你會怎麼做?  

// 樸素演算法的處理就是 O(n^2)
prev.forEach((p, ip) => {
    curr.forEach((c, ic) => {
    // includes 時間複雜度為 O(n),假設使用者用 set,has 是常數級的,暫且視此處也為常數級
    if (p !== c && prev.includes(c)) {
      // 解綁
    } 
    
    if (p !== c && !prev.includes(c)) {
      // 繫結
    } 
  })
})

如果加個 diffValue 作為標誌的話,演算法就為:

const a = {d: 0};
const b = {d: 0};
const c = {d: 0};

const prev = [a, b];
const curr = [b, c];

curr.forEach(c => c.d = 1);
prev.forEach(p => {
    if (p.d === 0) {
            // 解綁
  }
  p.d = 0;
})
curr.forEach(c => {
  if (c.d === 1) {
    // 繫結
  }
  c.d = 0;
})

至此,依賴關係處理完了,該 Derivation 上收集了使用的 OV,每個 OV 也收集了派生的 Derivation。並且把該 Derivation、之前收集的依賴的狀態置為了 UP_TO_DATE。

derivation.dependenciesState = IDerivationState.UP_TO_DATE
OV.lowestObserverState = IDerivationState.UP_TO_DATE

新繫結的依賴狀態為 NOT_TRACKING | UP_TO_DATE。

setter 裡幹了啥?

同樣以裝飾器修飾的屬性為例:

write(key: PropertyKey, newValue) {
        const instance = this.target
        // 拿到 OV
        const observable = this.values.get(key)
           // 處理計算值情況
        if (observable instanceof ComputedValue) {
            observable.set(newValue)
            return
        }
        // enhance 新值,Object.is 對比新舊值
        newValue = (observable as any).prepareNewValue(newValue)

        if (newValue !== globalState.UNCHANGED) {
              // 值變化
            // ...
            (observable as ObservableValue<any>).setNewValue(newValue)
            // ...
        }
    }

setNewValue(newValue: T) {
        const oldValue = this.value
        this.value = newValue
        this.reportChanged()
        // ...
    }

reportChanged() {
        startBatch()
              // 通知變化
        propagateChanged(this)
        endBatch()
    }

通知變化其實做了三件事情:

  1. 把 OV 的狀態變為 STALE
  2. 遍歷 OV 繫結的所有 Derivation,並處理
  3. 處理完一個 Derivation 則變更其狀態為 STALE
export function propagateChanged(observable: IObservable) {
    // invariantLOS(observable, "changed start");
    if (observable.lowestObserverState === IDerivationState.STALE) return
    observable.lowestObserverState = IDerivationState.STALE

    // Ideally we use for..of here, but the downcompiled version is really slow...
    // 如果被解除 observableValue 和 Observer 的繫結關係,這裡就不會遍歷到。
    observable.observers.forEach(d => {
        if (d.dependenciesState === IDerivationState.UP_TO_DATE) {
            // ...
              // 遍歷 OV 繫結的所有 Derivation,並處理
            d.onBecomeStale()
        }
        d.dependenciesState = IDerivationState.STALE
    })
    // invariantLOS(observable, "changed end");
}

d.onBecomeStale() 幹了啥呢?

其實就是再把該 Derivation 加入計劃表,排期執行 Reaction,重複我們上面的流程。

onBecomeStale() {
        this.schedule()
    }
Reaction 流程概覽

Computed Value

CV 是比較特殊的存在,即作為依賴,也作為派生。它是用它的副作用裡的依賴,是它內部依賴的派生。

流程

在 Mobx 裡也是用一個類 ComputedValue 來管理:

class ComputedValue {
  dependenciesState = IDerivationState.NOT_TRACKING // 作為派生的初始狀態
  lowestObserverState = IDerivationState.UP_TO_DATE // 作為依賴的初始狀態
    observing: IObservable[] = [] // CV 作為派生,收集的所有依賴
  newObserving = null // 每 batch 執行中新收集的依賴
  observers = new Set<IDerivation>() // CV 作為依賴,收集的所有派生
  // ...
  constructor(options: IComputedValueOptions<T>) {
               // 檢錯機制,引數必須含 get 
        invariant(options.get, "missing option for computed: get")
            // getter 回撥作為內部依賴的派生
        this.derivation = options.get!
        this.name = options.name || "ComputedValue@" + getNextId()
        // 處理 setter
            // ...
            // 對於新舊計算結果的對比方法,預設 Object.is
        this.equals =
            options.equals ||
            ((options as any).compareStructural || (options as any).struct
                ? comparer.structural
                : comparer.default)
            // getter 回撥計算的上下文
        this.scope = options.context
                // 是否必須要求在副作用內使用計算屬性
        this.requiresReaction = !!options.requiresReaction
            // 是否一直強制繫結計算屬性以及內部依賴。 (預設當計算屬性沒被用時,會同步解綁計算屬性與其內部依賴)
        this.keepAlive = !!options.keepAlive
    }
}

每次當計算屬性被訪問時,會觸發內部 get 方法,主要做兩件事:

  1. 通知被觀察
  2. 評估是否需要計算,若需要,則處理一些狀態改變。
public get(): T {
        if (this.isComputing) fail(`Cycle detected in computation ${this.name}: ${this.derivation}`)
        if (globalState.inBatch === 0 && this.observers.size === 0 && !this.keepAlive) {
            // 在非副作用裡訪問,簡單計算出返回值
            if (shouldCompute(this)) {
                this.warnAboutUntrackedRead()
                startBatch() // See perf test 'computed memoization'
                this.value = this.computeValue(false)
                endBatch()
            }
        } else {
              // 在副作用裡訪問
            // 通知被觀察,加入 Reaction.newObserving,之後會建立起計算屬性與其派生的繫結關係
            reportObserved(this)
            // 評估作為 Derivation 是否需要計算
            // 若需要,重新計算完後,自身作為 D 的狀態變為 U_T_D 。依賴狀態變更為 U_T_D
            // 若值有改變,則改變自身作為 OV 的狀態為 STALE,收集的觀察者(第一次讀取時沒有)的狀為 STALE 
            if (shouldCompute(this)) if (this.trackAndCompute()) propagateChangeConfirmed(this)
        }
        const result = this.value!

        if (isCaughtException(result)) throw result.cause
        return result
    }
評估計算

第一次訪問肯定需要計算的,我們來看下評估計算的方法:

export function shouldCompute(derivation: IDerivation): boolean {
    switch (derivation.dependenciesState) {
        case IDerivationState.UP_TO_DATE:
            return false
        case IDerivationState.NOT_TRACKING: // 第一次訪問時
        case IDerivationState.STALE:
            return true
        case IDerivationState.POSSIBLY_STALE: {
            // 暫時跳過
        }
    }
}

在知道允許計算後,就開始計算和追蹤依賴了

private trackAndCompute(): boolean {
        // ...
        const oldValue = this.value
        // 有沒有解除計算屬性與其內部依賴的繫結關係,第一次肯定是沒有繫結關係的
        const wasSuspended =
            /* see #1208 */ this.dependenciesState === IDerivationState.NOT_TRACKING
        // 新計算的值
        const newValue = this.computeValue(true)
        const changed =
            wasSuspended ||
            isCaughtException(oldValue) ||
            isCaughtException(newValue) ||
            !this.equals(oldValue, newValue)
        if (changed) {
            // 若有改變則賦新值
            this.value = newValue
        }
        return changed
    }

computeValue(track: boolean) {
        this.isComputing = true
        globalState.computationDepth++
        let res: T | CaughtException
        if (track) {
            // 不僅計算、也追蹤內部依賴
            res = trackDerivedFunction(this, this.derivation, this.scope)
        } else {
              // 簡單重新計算
            if (globalState.disableErrorBoundaries === true) {
                res = this.derivation.call(this.scope)
            } else {
                try {
                    res = this.derivation.call(this.scope)
                } catch (e) {
                    res = new CaughtException(e)
                }
            }
        }
        globalState.computationDepth--
        this.isComputing = false
        return res
    }

trackDerivedFunction 很熟悉了,在講解 AutoRun 時分析過了,主要就是幹了三件事,其實就是計算和追蹤依賴:

  1. 因為派生即將執行,所以改變派生與依賴的狀態為 U_T_D
  2. 執行派生
  3. 建立派生與依賴的繫結關係

如果結果有改變的話,就執行 propagateChangeConfirmed(this),也就是改變 CV 作為依賴、以及其派生的狀態為 STALE 了。

export function propagateChangeConfirmed(observable: IObservable) {
    // invariantLOS(observable, "confirmed start");
    // 讓 computedValue 作為 OV ,改變自身狀態與其收集的 Derivation 都為不穩定
    if (observable.lowestObserverState === IDerivationState.STALE) return
    observable.lowestObserverState = IDerivationState.STALE
        // 第一次訪問計算屬性時,還未建立起派生與計算屬性的繫結關係,所以 observers 為空
      // 之後訪問的情況下,就會把派生的狀態由 P_S 轉為 STALE 了
    observable.observers.forEach(d => {
        if (d.dependenciesState === IDerivationState.POSSIBLY_STALE)
            d.dependenciesState = IDerivationState.STALE
        else if (
            d.dependenciesState === IDerivationState.UP_TO_DATE // this happens during computing of `d`, just keep lowestObserverState up to date.
        ) // 當派生已經開始重新處理時會遇到這個情況,此時不需要改變計算屬性作為 OV 的狀態和派生的狀態了,因為派生已經重新處理了,並且也會拿到最新的計算值,此時直接把計算屬性作為 OV 的狀態設為 U_T_D 就好
          // 比如,計算屬性的派生是與依賴 A 與計算屬性繫結的
          // 某個 action 裡面先改變了計算屬性的深依賴值,再改變依賴 A 的值
                    // 此時派生的狀態會先變 P_S ,再變為 STALE,
          // 在一輪 batch 結束後,重新處理派生 Reaction,會直接重新計算計算屬性的值,走到這個判斷條件內,不需要再管派生應不應該重新處理了,人家已經由依賴 A 的變化確定要處理了。
            observable.lowestObserverState = IDerivationState.UP_TO_DATE
    })
    // invariantLOS(observable, "confirmed end");
}

之後在計算屬性的派生接著處理,就會把計算屬性作為依賴的狀態和派生自己的狀態變為 U_T_D,等著下一次依賴改變再次處理了。

計算屬性的派生內其他依賴改變

這種情況會再次讀取計算屬性的值,但由於 shouldCompute 會評估計算屬性的派生狀態為 U_T_D,也就是其深依賴沒有改變,所以會直接取上一次計算的結果來使用,不會再有其他任何處理。

計算屬性的依賴改變

這種情況下就會按依賴變化的正常流程走,AutoRun 裡講過,觸發深依賴的 setter,改變深依賴和派生(計算屬性)的狀態為 STALE,然後執行派生的 onBecomeStale() 方法。

onBecomeStale() 方法對於 Reaction 而言就是加入計劃表,等待 batch 結束統一再次處理一遍 Reaction。對於計算屬性而言稍微有點變化:

  1. 會改變計算屬性作為 OV 的狀態為 P_S,改變計算屬性的派生的狀態為 P_S
  2. 把計算屬性的派生列入計劃表
onBecomeStale() {
        propagateMaybeChanged(this)
    }

export function propagateMaybeChanged(observable: IObservable) {
    // invariantLOS(observable, "maybe start");
    if (observable.lowestObserverState !== IDerivationState.UP_TO_DATE) return
    observable.lowestObserverState = IDerivationState.POSSIBLY_STALE

    observable.observers.forEach(d => {
        if (d.dependenciesState === IDerivationState.UP_TO_DATE) {
            d.dependenciesState = IDerivationState.POSSIBLY_STALE
            if (d.isTracing !== TraceMode.NONE) {
                logTraceInfo(d, observable)
            }
              // 將 Reaction 加入計劃表,等待重新處理
            d.onBecomeStale()
        }
    })
    // invariantLOS(observable, "maybe end");
}

然後等深依賴的 batch 結束,就會在計劃表取出 Reaction 做處理,回到 Autorun 裡的邏輯:

runReaction() {
        if (!this.isDisposed) {
            // 開啟一個事務處理,因為執行 cb 的過程中可能會再加 Reaction 到計劃表(比如依賴更新)
            startBatch()
            this._isScheduled = false
            // 判斷 Reaction 收集的依賴狀態
            // 如狀態機所示,只有在 NO_TRACKING | STALE | 判斷 COMPUTED 值變化時才會執行 Reaction 
            if (shouldCompute(this)) {
                this._isTrackPending = true

                try {
                      // 處理 cb 
                    this.onInvalidate()
                    // ...
                } catch (e) {
                    this.reportExceptionInDerivation(e)
                }
            }
            endBatch()
        }
    }
評估計算

此時派生的狀態就是 P_S,評估計算時就會走到下面的邏輯:

  1. 找到派生依賴的計算屬性,並重新計算,改變計算屬性深依賴和自身作為派生的狀態為 U_T_D
  2. 若重新計算值有變化,則會改變計算屬性作為 OV 的狀態和計算屬性派生的狀態為 STALE,接著處理派生,讓派生回撥重新執行,重新建立依賴繫結關係。
  3. 若重新計算值沒變化,則直接返回舊值,改變派生和計算屬性作為 OV 的狀態為 U_T_D,阻止派生繼續處理。
export function shouldCompute(derivation: IDerivation): boolean {
    switch (derivation.dependenciesState) {
        case IDerivationState.UP_TO_DATE:
            return false
        case IDerivationState.NOT_TRACKING:
        case IDerivationState.STALE:
            return true
        case IDerivationState.POSSIBLY_STALE: {
            // state propagation can occur outside of action/reactive context #2195
            const prevAllowStateReads = allowStateReadsStart(true)
            // 此處對 CV 的 get 不需要 reportObserved (untrackedStart 的作用),之後會再執行進行收集
            // 這裡的主要目的是:判斷重新計算的值有沒有改變,然後根據結果做一些狀態變更
            const prevUntracked = untrackedStart() // no need for those computeds to be reported, they will be picked up in trackDerivedFunction.
            const obs = derivation.observing, // 拿到所有 OV
                l = obs.length
            for (let i = 0; i < l; i++) {
                const obj = obs[i]
                // 找到 CV 的 OV
                if (isComputedValue(obj)) {
                    if (globalState.disableErrorBoundaries) {
                        obj.get()
                    } else {
                        try {
                              // 再次呼叫 get 重新計算,具體邏輯上面分析過
                            obj.get()
                        } catch (e) {
                            // we are not interested in the value *or* exception at this moment, but if there is one, notify all
                            // 如果 CV getter 執行異常,那就預設讓副作用繼續執行一次
                            untrackedEnd(prevUntracked)
                            allowStateReadsEnd(prevAllowStateReads)
                            return true
                        }
                    }
                    // if ComputedValue `obj` actually changed it will be computed and propagated to its observers.
                    // and `derivation` is an observer of `obj`
                    // invariantShouldCompute(derivation)
                      // 若重新計算有變化了,其派生的狀態會變成 STALE
                    if ((derivation.dependenciesState as any) === IDerivationState.STALE) {
                        untrackedEnd(prevUntracked)
                        allowStateReadsEnd(prevAllowStateReads)
                        // 允許派生執行
                        return true
                    }
                }
            }
              // 如果重新計算值沒有變化,則重置派生與計算屬性作為依賴的狀態為 U_T_D
            changeDependenciesStateTo0(derivation)
            untrackedEnd(prevUntracked)
            allowStateReadsEnd(prevAllowStateReads)
            // 不允許派生繼續執行
            return false
        }
    }
}

以上,就達到了計算屬性依賴無變化時直接應用舊計算值(避免多餘計算)、計算屬性依賴變化且重新計算值變化時才會重新處理副作用(避免無效副作用)的目的。

React render

類元件

我們可以用 @observer 去裝飾一個元件:

import { observer } from 'mobx-react';

@observer
class AComponent {
  render() {
    return ...;
  };
}

實際 observer 做的核心工作就把 render 函式作為派生(用一個派生包住),然後每次 track 重新執行 render 的行為來收集依賴,當依賴改變的時候,就觸發 React.Component.prototype.forceUpdate() 去強制重新執行 render 收集依賴、更新檢視。

N.B. observer 裝飾後,React 的一些 lifecycle 鉤子無法觸發,所以其實內部還做了一些偽造鉤子的操作比如 shouldUpdate、willUnMount 以及一些優化和 fix bug 的操作,對於這些操作這裡跳過,只講核心原理程式碼。

我們來看看原始碼裡是怎樣實現的:

export function observer<T extends IReactComponent>(component: T): T {
    // ... 錯誤操作的報警

    // ... 處理 ForwardRef 
  
    // 處理 Function component  暫且跳過
    if (
        typeof component === "function" &&
        (!component.prototype || !component.prototype.render) &&
        !component["isReactClass"] &&
        !Object.prototype.isPrototypeOf.call(React.Component, component)
    ) {
        return observerLite(component as React.StatelessComponent<any>) as T
    }
        // Class Component
    return makeClassComponentObserver(component as React.ComponentClass<any, any>) as T
}

export function makeClassComponentObserver(
    componentClass: React.ComponentClass<any, any>
): React.ComponentClass<any, any> {
      // 元件原型
    const target = componentClass.prototype

    if (componentClass[mobxObserverProperty]) {
          // 錯誤操作報警
        const displayName = getDisplayName(target)
        console.warn(
            `The provided component class (${displayName}) 
                has already been declared as an observer component.`
        )
    } else {
          // 表示元件已被 Mobx 作為觀察者
        componentClass[mobxObserverProperty] = true
    }
        // 錯誤報警
    if (target.componentWillReact)
        throw new Error("The componentWillReact life-cycle event is no longer supported")
        // 實現 shouldComponentUpdate
    if (componentClass["__proto__"] !== PureComponent) {
        if (!target.shouldComponentUpdate) target.shouldComponentUpdate = observerSCU
        else if (target.shouldComponentUpdate !== observerSCU)
            // n.b. unequal check, instead of existence check, as @observer might be on superclass as well
            throw new Error(
                "It is not allowed to use shouldComponentUpdate in observer based components."
            )
    }

    // 將 Props、State 包裝成 OV
    makeObservableProp(target, "props")
    makeObservableProp(target, "state")
        // 原始 render
    const baseRender = target.render
    // 被攔截的 render,只首次 mount 會調這個
    target.render = function () {
          // 原始 render 外部包了一層派生
        return makeComponentReactive.call(this, baseRender)
    }
    patch(target, "componentWillUnmount", function () {
        if (isUsingStaticRendering() === true) return
          // 元件解除安裝時,解綁派生與依賴的繫結,避免記憶體洩漏
        this.render[mobxAdminProperty]?.dispose()
        this[mobxIsUnmounted] = true

        if (!this.render[mobxAdminProperty]) {
            // Render may have been hot-swapped and/or overriden by a subclass.
            const displayName = getDisplayName(this)
            console.warn(
                `The reactive render of an observer class component (${displayName}) 
                was overriden after MobX attached. This may result in a memory leak if the 
                overriden reactive render was not properly disposed.`
            )
        }
    })
    return componentClass
}

function makeComponentReactive(render: any) {
    if (isUsingStaticRendering() === true) return render.call(this)

      // 處理 forceUpdate 帶來的副作用 ...

    const initialName = getDisplayName(this)
    const baseRender = render.bind(this)

    let isRenderingPending = false

    // 建立一個派生, 帶來的副作用就是第二個回撥引數
    const reaction = new Reaction(`${initialName}.render()`, () => {
        if (!isRenderingPending) {
            // N.B. Getting here *before mounting* means that a component constructor has side effects (see the relevant test in misc.js)
            // This unidiomatic React usage but React will correctly warn about this so we continue as usual
            // See #85 / Pull #44
            isRenderingPending = true
            if (this[mobxIsUnmounted] !== true) {
                let hasError = true
                try {
                    // 處理 forceUpdate 帶來的副作用 ...
                      // forceUpdate 強制重渲染
                    if (!this[skipRenderKey]) Component.prototype.forceUpdate.call(this)
                    hasError = false
                } finally {
                    // 處理 forceUpdate 帶來的副作用 ...
                    if (hasError) reaction.dispose()
                }
            }
        }
    })

    reaction["reactComponent"] = this
    reactiveRender[mobxAdminProperty] = reaction
      // 之後 forceUpdate 的時候,重新執行的 render 都只是 reactiveRender
    this.render = reactiveRender

    function reactiveRender() {
        isRenderingPending = false
        let exception = undefined
        let rendering = undefined
        // 為該 reaction 派生收集原 render 函式內的依賴
        reaction.track(() => {
            try {
                  // 執行原 render 函式,拿到虛擬節點
                rendering = _allowStateChanges(false, baseRender)
            } catch (e) {
                exception = e
            }
        })
        if (exception) {
            throw exception
        }
          // 返回虛擬節點給 React
        return rendering
    }

    return reactiveRender.call(this)
}
函式元件

函式元件要達到的目的和類元件是一致的,都是 rerender 重新收集依賴,依賴變化觸發 rerender。但是函式元件不能用 forceUpdate 這個 API,所以 Mobx 內部用了 React hooks 的小 trick 去實現了 forceUpdate 的效果。

由於這種小 trick 帶來的副作用更多,所以這部分 mobx-react-light 裡的處理很冗餘,提煉程式碼來做講解:

function observerLite(baseFuncComponent) {
  return function(props) {
    const [tick, setTick] = useState(0);
    // 利用 useState 偽造 forceUpdate
    function forceUpdate() {
      setTick(tick + 1);
    }
    // 造一個函式元件的派生,useMemo 保證派生不會 rebuild
    // 元件內依賴變化時,invoke forceUpdate,rerender
    const r = useMemo(() => new Reaction('包裹函式元件的派生', forceUpdate), []);
    // 元件解除安裝時解綁派生與依賴,避免記憶體洩漏
    useEffect(() => () => r.dispose(), []);
    
    let vnodes = null;
    // 每輪 rerender 重新執行函式元件,追蹤依賴
    r.track(() => {
      vnodes = baseFuncComponent({...props, tick});
    })
        // 返回虛擬節點給 React
    return vnodes;
  }
}

其餘 API

action

以裝飾器 action 修飾函式 fn 為例,其實就是重寫了 fn 的描述符,把函式體由 createAction 包了一層:

return {
  // name 函式名、descriptor.value 函式體
  value: createAction(name, descriptor.value),
  enumerable: false,
  configurable: true, // See #1477
  writable: true // for typescript, this must be writable, otherwise it cannot inherit :/ (see inheritable actions test)
}
export function createAction(actionName: string, fn: Function, ref?: Object): Function & IAction {
    // ...
      // 外部呼叫 fn 時,真正執行的是這個方法
    const res = function() { 
          // executeAction 內部核心工作就是讓 fn 的執行處於一輪事務當中
        return executeAction(actionName, fn, ref || this, arguments)
    }
    ;(res as any).isMobxAction = true
    // ...
    return res as any
}
export function executeAction(actionName: string, fn: Function, scope?: any, args?: IArguments) {
    const runInfo = _startAction(actionName, scope, args)
    try {
        return fn.apply(scope, args)
    } catch (err) {
        runInfo.error = err
        throw err
    } finally {
        _endAction(runInfo)
    }
}

// startAction 除了 startBatch 以外的操作,都是為了確實新開啟一輪事務的純淨性,不被之前上下文的操作所影響。
export function _startAction(actionName: string, scope: any, args?: IArguments): IActionRunInfo {
    // ...
    let startTime: number = 0   
      // 在 action 裡,對 OV 的讀取不收集方法 fn。因為 action 方法並不是副作用,而是要改變依賴的動作。
    const prevDerivation = untrackedStart()
    startBatch() // 開啟一輪新事務
      // 允許對依賴寫
    const prevAllowStateChanges = allowStateChangesStart(true)
    // 允許對依賴讀
    const prevAllowStateReads = allowStateReadsStart(true)
    // 記錄該輪事務的一些資訊,方便 endAction 時回退,保持開啟事務前的狀態純淨。
    const runInfo = {
        prevDerivation,
        prevAllowStateChanges,
        prevAllowStateReads,
        notifySpy,
        startTime,
        actionId: nextActionId++,
        parentActionId: currentActionId
    }
    currentActionId = runInfo.actionId
    return runInfo
}

// 除了結束事務的操作,其餘都是根據記錄的該輪事務的一些資訊,回退保持開啟事務前的狀態純淨。
export function _endAction(runInfo: IActionRunInfo) {
    if (currentActionId !== runInfo.actionId) {
        fail("invalid action stack. did you forget to finish an action?")
    }
    currentActionId = runInfo.parentActionId

    if (runInfo.error !== undefined) {
        globalState.suppressReactionErrors = true
    }
      // 回退開啟事務前依賴的改變許可權
    allowStateChangesEnd(runInfo.prevAllowStateChanges)
    // 回退開啟事務前依賴的讀取許可權
    allowStateReadsEnd(runInfo.prevAllowStateReads)
    // 結束事務,準備批量處理收集的 Reaction
    endBatch()
      // 回退開啟事務前的派生追蹤
    untrackedEnd(runInfo.prevDerivation)
    // ...
    globalState.suppressReactionErrors = false
}

其實看原始碼一目瞭然了,主要目的就是讓 action 的函式執行身處於一輪新的事務中,好處就是為了多次改變某 Derivation 的依賴時,只處理一次。迴歸上文講得 transaction 的概念:

一個 Action 開始和結束時同時伴隨著事務的啟動和結束,確保 Action 中(可能多次)對狀態的修改只觸發一次 Reaction 的重新執行。

額外 API

額外的 API 在熟悉了上文的所有內容後,閱讀起來應該比較簡單了,鑑於 API 太多,不一一做分析,感興趣自行挖掘。

Mobx 設計思想

Mobx 作者 Michel Weststrate 有在一篇推文中闡述過 Mobx 設計理念,但是有點過於細節,不熟悉 Mobx 機制的同學可能不太看得懂。以下,在基於這篇推文結合上述原始碼,我用中文提煉一下,感興趣可以去看原文。

對狀態改變作出反應永遠好過於對狀態改變作出動作

針對這點其實與 Vue 響應式 or Redux 傳遞的理念相同,就是資料驅動

再分析這句話,“作出反應” 意味著狀態與副作用的繫結關係由框架(庫)給你做好,狀態改變自動通知到副作用,不用使用者(開發者)人為地處理。

“作出動作”則是在使用者已知狀態更改的情況下,手動去通知副作用更新。 這起碼就有一個操作是使用者必做的:手動在副作用內訂閱狀態的變化,這至少帶來兩個缺陷:

  1. 無法保證訂閱量的冗餘性,可能訂閱多了可能少了,導致應用出現不符合預期的情況。
  2. 會讓業務程式碼變得更 dirty,不好組織

最小的、一致的訂閱集

以 render 作為副作用舉例,假如 render 裡有條件語句:

render() {
  if (依賴 A) {
    return 元件 1;
  }
  return 依賴 B ? 元件 2 : 元件 3;
}

首先,如果交給使用者手動訂閱,必須只能依賴 A、B 的狀態一起訂閱才行,如果訂閱少了無法出現預期的 re-render。

然後交給框架去做處理怎樣才好? 依賴 A、B 一起訂閱當然沒毛病,但是假設依賴 A、B 初始化時都有值,我們有必要讓 render 訂閱依賴 B 的狀態嗎?

沒必要,為什麼?想一想如果此時依賴 B 的狀態變化了 re-render 呈現的效果會有什麼不同嗎?

所以在初始化時就訂閱所有的狀態是冗餘的,假如應用程式複雜、狀態多了,沒必要的記憶體分配就會更多,對效能有損耗。

故 Mobx 實現了執行時處理依賴的機制,保證副作用繫結的是最小的、一致的訂閱集。原始碼參見上述 “getter 裡幹了啥?” 與 “處理依賴” 章節。

派生計算的合理性

說人話就是:杜絕丟失計算、冗餘計算

丟失計算:Mobx 的策略是引入狀態機的概念去管理依賴與派生,讓數學的邏輯性保證不會丟失計算。

冗餘計算:

  1. 對於非計算屬性狀態,引入事務概念,保證同一批次中所有對狀態的同步更改,狀態對應的派生只計算一次。
  2. 對於計算屬性,計算屬性作為派生時,當其依賴變化,計算屬性不會立即重新計算,會等到計算屬性自身作為狀態所繫結的派生再次用到計算屬性值時才去重新計算。並且計算出相同值會阻止派生繼續處理。

通用性(筆者補充)

就 Mobx 庫本身,與 UI render 沒有繫結關係,與 event loop 中非同步機制沒繫結關係。

所以 Mobx 不像 Vue 2.x 響應式處理一樣,需要收集 Wachter 然後趕在 ui render 前非同步迭代處理 Wachter 對應的副作用。更新粒度也不一樣,Vue 2.x 是元件, Mobx 就是副作用,副作用可以但不僅是元件。

不知道 Vue 3.x 把響應式抽成一個 package 後還是不是這樣,沒研讀過其原始碼了。(不過 Vue 2.x 原始碼記得當時研究了很久,現在也忘得差不多了,現在再去撿起來又覺得耗時且帶來的收益不大,可惡又無奈的學習邊際效應 -_-||)

故:Mobx 適用於任一使用 ES 語法的場景。

最後

以往文章大多記錄在 github,之後會嘗試在社群輸出,覺得有幫助的同學可以關注,共同進步。

預告下一篇文章內容:mutable 與 immutable 的理念區別,Flux 思想,單/雙向資料流的各自優勢與痛點,mobx 與 redux、recoil 及一些新生代狀態管理庫的橫向對比。

不過下一篇可能更新得比較慢,有個想做的工具要先寫,下一篇文章的內容也有在群裡跟神光光神提過,可能光神先產出我就不釋出了(逃

相關文章