天天看點

MobX設計思想與實作

最近在搜集MobX相關資料,意外的找到MobX作者一篇 深度解析MobX工作機制的blog ,看過之後受益匪淺。本篇文章将結合MobX源碼來整理并了解這篇blog的内容。這篇部落客要是想為讀者解讀MobX的實作機制,其中穿插介紹了一些作者的設計思路。

幾個重要的概念

首先,作者先澄清了幾個MobX中的重要概念

  • Observable State, 所有可以改變的值。
  • Computed Value(又稱Derivation), 可以通過Observable State直接計算(pure function)得出的值。
  • Reaction, 與Computed Value類似也是基于Observable State 。但是不是傳回一個結果,而是産生一個動作(side effects)
  • Action, 所有修改Observable State的動作

這幾個概念的關系如下圖:

MobX設計思想與實作

這個圖解釋了,在MobX體系裡各個角色的作用。

首先是發生一個Action修改State,接着State的更新會自動觸發與其相關聯的Derivation 和Reaction。

需要注意的是Derivation在這張圖中的雙重角色。在觀察者模式視角下,它不僅是observable也是observer。或者說對于State來說,它是一個observer監視State的變化;但是對于Reaction來說,它可能還是一個observable,它的變化會引發Reaction發生。

一個簡單的示例

接着作者給出了一個使用MobX的

例子

,來說明這些概念。

class Person {
  @observable firstName = "Michel";
  @observable lastName = "Weststrate";
  @observable nickName;
  
  @computed get fullName() {
    return this.firstName + " " + this.lastName;
  }
}
const michel = new Person();
// Reaction: log the profile info whenever it changes
autorun(() => console.log(person.nickName ? person.nickName : person.fullName));
// Example React component that observes state
const profileView = observer(props => {
  if (props.person.nickName)
    return <div>{props.person.nickName}</div>
  else
    return <div>{props.person.fullName}</div>
});
// Action:
setTimeout(() => michel.nickName = "mweststrate", 5000)
React.render(React.createElement(profileView, { person: michel }), document.body);      

這個例子的邏輯很簡單,通過判斷Person對象是否存在nickName屬性來決定展示在界面(profileView)上的内容(nickName還是fullName)。

它們的依賴關系大緻如下圖:

MobX設計思想與實作

如果你仔細再看這張圖中的fullName,大概你會更清楚為什麼我之前說Derivation有着雙重角色。實際上Derivation的特殊性也展現在

實作

裡。

在MobX實作裡,充當observable角色的類都會有一個lowestObserverState标示目前的狀态(Stale,UP_TO_DATE等)。(像 之前文章 裡提到的observable體系裡類例如ObservableValue,ObservableArray都有這樣的屬性。) 同樣的observer也會有一個dependenciesState标示目前狀态。 然而ComputedValue類卻同時擁有這兩個屬性。

動态更新依賴

回到剛剛的示例中,在最後我們觸發了一個Action。

// Action:
setTimeout(() => michel.nickName = "mweststrate", 5000)      

為Person對象添加nickName 屬性。是以界面profileView也會發生相應的變化。此時的依賴關系已經發生了微妙的變化。

MobX設計思想與實作

profileView已經不再依賴或者說監視fullName。如果這時,firstName或者fullName發生了變化,也不會對profileView有任何影響。

這就是MobX裡的動态更新依賴。這樣的設計,好處在于保證observer隻依賴于它需要的依賴。永遠也不會出現undersubscribe (forgetting to listen for updates leading to subtle staleness bugs)或者oversubscribe (continue subscribing to a value or store that is no longer used in a component)。

MobX能實作依賴動态更新,是因為它的依賴關系是由架構在運作時計算得到的。或者說,這個依賴關系并不是由使用者在寫代碼時手動的去關聯起來,而是由架構自己在運作時自動确定的。

比如在示例中,當執行profileView部分時:

const profileView = observer(props => {
  if (props.person.nickName)
    return <div>{props.person.nickName}</div>
  else
    return <div>{props.person.fullName}</div>
});      

它首先判斷是否存在nickName屬性,如果不存在就傳回fullName屬性,在執行profileView部分時就用到了nickName和fullName兩個observable,是以現在profileView就依賴于nickName和fullName

但是注意fullName可不是一個普通的observable,它是一個computed value。同樣執行fullNamegetter部分:

@computed get fullName() {
    return this.firstName + " " + this.lastName;
  }      

是以fullName又依賴于firstName和lastName。

整個過程如下圖:

MobX設計思想與實作

這樣我們的依賴關系就形成了。

這裡展現出了MobX的設計思想:

Reacting to state changes is always better than acting on state changes.

這句話初看時似懂非懂,reacting to change和acting on change似乎沒什麼差别。實際上這裡一個細微的用詞決定change發生後動作的主動權歸屬。acting的主動權在使用者,動作是否發生,發生哪些動作都由使用者來決定(依賴關系由使用者手動訂閱);而reacting的主動權在架構(依賴關系由架構自動生成)。

同樣當Action被觸發,profileView重新計算自己的依賴關系,這次nickName存在,不會用到fullName,是以profileView依賴更新,隻依賴于nickName.

這一部分的最後提一下這個

依賴更新的算法

。本質上是一個兩個數組無重複合并的算法,MobX的實作把這個算法的時間複雜度降到了o(n)。具體可以參考

這篇文章

從懶加載到MobX響應機制

MobX設計思想與實作

讓我們再回到這張圖,不知道你是否注意到此時,fullName不再被監視時,它的狀态已經被修改為lazy。 這代表着,如果現在修改firstName後者lastName,fullName不會立即執行。這就是MobX的性能優化之一——懶加載機制。

想要深入了解懶加載機制是如何實作的,需要我們首先明白MobX是如何響應變化的。當一個action産生時MobX内部發生了什麼。

仍然拿之前的例子來說明,假設這樣一個場景,依賴關系仍然是完整時(profileView仍然依賴fullName),此時firstName被修改。

  1. action發生,firstName值被修改,firstName狀态變更為stale
MobX設計思想與實作

2. firstName通知它的observer即fullName,fullName狀态變更為possible change

MobX設計思想與實作

3. fullName通知它的observer即profileView,profileView狀态變更為possible change。

MobX設計思想與實作

4. MobX監控到所有的狀态變更和修改都已經完成

5. MobX通知fullName開始執行, fullName通過firstName的新值重新計算自己的值,fullName修改firstName和自己的狀态為new

MobX設計思想與實作

6. 這裡出現了一個分支,通過fullName的新值和舊值做比較

6.1 如果fullName新值和舊值相同,即fullName其實沒有發生變化,則直接修改profileView狀态為new,不需要執行

6.2 如果fullName确實發生了變化,則profileView重新執行,并且修改自己的狀态為new

至此一次完整的更新結束。

如果把上面的過程做一個小結,我大緻可以把它分為兩個階段:

  • 冒泡階段,即被修改的observable去通知它的observer修改狀态,這個過程是級聯的(或者說是遞歸發生的) 。在冒泡階段除了修改狀态,實際上并沒有發生其他事情。
  • 執行階段,當冒泡階段結束,所有的狀态都已經被修改完成後,開始執行需要的操作。

為什麼一定要分成兩個階段而不是修改狀态使立即執行呢?

這實際上也是MobX性能優化的一個設計。它的思路實際就是批處理。因為上面的例子隻是一個非常簡單的更新。當有多個action一起發生的時候,是以如果每次都立即執行,可能每個action都導緻同一個computed value會需要重複執行很多次,求出很多無用的中間結果。 而等到所有狀态更新和action都結束後,再執行求值,就隻需要執行一次求出最終的結果。

到這裡,我比較在意的是MobX的實作是怎麼區分這兩個階段的。它怎麼知道什麼時候冒泡階段完全結束了需要開始執行階段論。這牽扯到MobX的另一個概念—— ​

trasnaction

​ 。

transaction

在資料庫中經常出現的概念,在資料庫中它定義了一個原子性的操作,并且用它來管理并發通路。

但是MobX裡的transaction不是幹這個的。它被引入的目的就是為了标注出一次完整的更新冒泡階段的開始和結束。在一個tranaction内是不會執行reaction或者computed value。

/**
 * During a transaction no views are updated until the end of the transaction.
 * The transaction will be run synchronously nonetheless.
 *
 * @param action a function that updates some reactive state
 * @returns any value that was returned by the 'action' parameter.
 */
export function transaction<T>(action: () => T, thisArg = undefined): T {
    startBatch()
    try {
        return action.apply(thisArg)
    } finally {
        endBatch()
    }
}      

也就是說,在一個當endBatch()被調用後,MobX就知道冒泡階段結束了,可以開始執行階段了。

這裡還要多說一句,transaction是一個底層的API,它在上層的封裝就是@action。這也是為什麼MobX的最佳實踐裡要求你把所有的state修改都加上action,實際就是把多個修改放到一個transaction裡,batch update。

還有一點是執行階段時的執行順序。

這裡的順序讓我聯想到了建構一個最小堆(或者最大堆)時的shift down的過程,在建立一個堆時需要保證它的子樹滿足最小堆的規則。是以會從n個結點的完全二叉樹最後一個分支節點floor( (n-2) / 2 )開始執行shif down。

MobX設計思想與實作

而執行階段也與之類似,它需要保證從整個依賴圖(注意不是樹)中最底層的一個父節點開始執行(實際就是按照拓撲排序的順序)。因為執行的規則是保證目前節點所有子節點的值是最新的。

MobX設計思想與實作

這個順序是如何确定的呢?在冒泡階段,MobX會把經過的Reaction放到一個待執行隊列裡。執行階段就直接從隊列裡取出執行。注意我這裡說的是Reaction而不包括Computed Value。是以fullName是不會加入隊列的。

但是我剛剛一直在說執行階段會先執行fullName。這其實并不沖突。在執行階段開始,MobX拿到profileView後,檢查它的所有依賴,結果找到了fullName是處于possible change狀态。它當然會先确定fullName的值,然後才能确定自己的值。

還記得我之前提到的懶加載嗎?它就是這樣實作的。因為Comupued Value不會加入隊列裡而是通過Reaction檢查依賴擷取的。是以如果Computed Value 沒有任何observer依賴,就不會有Reaction能到達它。它就當然不會被執行了。隻有當以後某個機會,它又與某個Reaction關聯起來後,那時才會被執行。

是以懶加載的本質是隻有冒泡階段沒有執行階段。

小結

本篇文章介紹了MobX裡的依賴動态更新機制以及變更響應機制的設計與實作。