【譯】深刻理解Mobx

https://user-gold-cdn.xitu.io/2019/3/8/16959647b2e6a1d2?w=800&h=800&f=png&s=26678

首先,先讓咱們看一下Mobx的核心概念。react

  1. 可觀察狀態(Observable state)。 任何能夠變異且能夠做爲計算值源的值都是state。Mobx能夠開箱即用地使絕大多數類型的值(基本類型,數組,類,對象)變成可觀察的(甚至深度觀察)。web

  2. 計算值(Computed values)。 任何可觀察的值可使用純函數計算得出任何值。計算值的範圍能夠從簡單的字符串到複雜的對象甚至dom操做上。計算值會懶惰地對狀態變化作出反應。算法

  3. 反應(Reactions)。 反應有點相似於計算值,可是它不產生新值,而是做爲橋樑銜接了響應式編程和命令式編程,產生一個反作用(I/O操做),例如打印到控制檯,發送網絡請求,更新dom樹等。編程

  4. 操做(Actions)。 操做是改變狀態(state)的主要手段。操做不是狀態改變後的反應,是改變的來源,例如用戶事件或web-socket鏈接,用以改變可觀察的狀態。後端

計算值和反應在本文章的後續中都稱爲衍生(derivations)。到目前爲止,這聽起來可能有點學術性,因此,咱們讓它具體點。在excel電子表格中,全部具備值的數據單元格都是可觀察狀態的,公式和圖標是能夠從數據單元格和其餘公式衍生的計算值。在屏幕上繪製數據單元格或公式的結果就是一個反應(reaction),改變數據單元格或公式就是一個操做(action)數組

下面這個例子結合了Mobx和React,而且包含了以上4個概念:緩存

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);
// This snippet is runnable in jsfiddle: https://jsfiddle.net/mweststrate/049r6jox/
view rawprofile.jsx hosted with ❤ by GitHub
複製代碼

咱們能夠畫一張依賴圖:網絡

https://user-gold-cdn.xitu.io/2019/3/8/169595a55efe3011?w=535&h=353&f=png&s=37605
Figure 1:profileView組件的依賴關係樹。fullName處於reactive mode,主動觀察firstName和lastName

這個應用中,狀態由可觀察狀態捕獲(藍色圖標)。綠色的fullName是計算值,由可觀察狀態firstName和lastName自動衍生得出。一樣,profileView由nickName和fullName衍生得出。profileView將經過產生反作用來響應狀態更改——它更新React組件樹。閉包

當使用Mobx時,依賴關係樹被最低限度定義。舉個例子,一旦profileView的有了nickName,且渲染再也不受fullName的值的影響,也不受firstName和lastName的影響,那麼他們之間全部的觀察者關係將被清除,Mobx將自動的簡化依賴樹,以下圖:併發

https://user-gold-cdn.xitu.io/2019/3/8/169595a55e6d593d?w=535&h=353&f=png&s=33768
Figure 2:當profileView具備nickName時的依賴關係樹,與Figure 1相反,fullName如今處於lazy mode,且再也不觀察firstName和lastName

Mobx老是使用最小化計算次數去產生狀態。接下來,我將介紹用於實現此目標的幾種策略,但在深刻了解計算值和反應如何與狀態同步以前,讓咱們首先描述Mobx背後的原理:

對狀態變化作出反應老是比對狀態變化作出動做要好。(Reacting to state changes is always better then acting on state changes.)

應用程序響應狀態更改的必要操做是一般會建立或更新一些值,大多數的操做(actions)管理着本地緩存。dom更新、批量更新值、請求後端,這些均可以被認爲變相地使緩存失效。要確保這些緩存保持同步,你須要訂閱(subscribe)將來的狀態更改,以便再次觸發你的操做。

可是觀察者模式有一個基本的問題:當你的應用變大時,你可能會犯錯,好比依然訂閱再也不使用的值或忘記訂閱一些值。

像flux風格的訂閱很容易出現這種超額訂閱的狀況。使用React時,你能夠經過在渲染中打印來判斷你的組件是否被超額訂閱了。Mobx會將打印出的超額訂閱數減小到0。這個想法很簡單但違反直覺:訂閱越多,從新計算越少。Mobx爲你管理數千個觀察者,你能夠有效地權衡內存的CPU週期。

超額訂閱也以很是微妙的形式存在。若是你訂閱了使用的數據,但並未在全部條件下訂閱,那麼你依然須要超額訂閱。例如,若是profileView組件訂閱了fullName,而且profileView有nickName,這就將超額訂閱。所以Mobx設計背後的一個重要原則是:

在運行時才能實現最小、一致地訂閱子集(A minimal, consistent set of subscriptions can only be achieved if subscriptions are determined at run-time)。

Mobx背後的第二個重要思想是,對於任何比TodoMVC更復雜的應用程序,一般須要一個數據圖而不是規範化的樹,以一種最佳方式存儲狀態。數據圖能夠實現參照一致性並避免數據重複,從而保證衍生值永遠不會過期。

Mobx如何有效地將全部衍生保持在一個一致地狀態?

答案是:不緩存,只有在須要時再從新計算衍生。這不是很昂貴嗎?Mobx認爲不是,反而這是很高效地。緣由是Mobx不會運行全部衍生,但確保參與reaction的computed values與可觀察狀態保持同步。這些衍生被稱爲響應式的。再次以excel舉例:只有當那些被觀察着的當前可見或被間接使用的公式發生變化時,纔會去從新計算值。

Lazy versus reactive evaluation

那麼反應沒有直接或間接使用的計算呢?你依然能夠隨時檢查計算值的值,如fullName。答案是簡單的:若是一個計算不是reactive的,它將被按需處理。就像一個普通的getter函數同樣。懶衍生若是沒有用了,將被簡單的垃圾回收。記住computed values老是須要使用純函數,由於對於純函數而言,它是懶衍生仍是直接使用並不重要。在相同的可觀察狀態下,computed values老是給出相同的結果。

運行計算

Reaction和Computed values在Mobx中都以相同的方式運行。當從新計算被觸發時,該函數將被壓入到衍生堆棧中。只要計算正在運行,每一個被訪問的observable都會將自身註冊爲衍生堆棧最頂層函數的依賴項。當computed value被須要了,若是該值處於reactive狀態,則該值能夠簡單的是最後已知的值。不然它將push本身到衍生堆棧中,切換到reactive模式並開始計算。

https://user-gold-cdn.xitu.io/2019/3/8/169595a56a1bb8ef?w=641&h=429&f=png&s=51072
Figure 4:profileView執行期間,一些observable state和computed values被觀察着。computed values可能從新計算,這會產生依賴關係樹,如Figure 1所示。

當一個計算完成後,將獲得在執行期間訪問的可觀察列表。在profileView的例子中,這個list將只包含nickName或nickName和fullName屬性。任何被移除的屬性都將再也不觀察(此時computed values可能會從反應模式返回到惰性模式),任何被添加的可觀察屬性將被觀察,直到下一次計算。例如,未來將更改firstName的值,fullName將會知道本身該被從新計算,從而profileView會從新計算。接下來會詳細解釋這一過程。

Propagating state changes

https://user-gold-cdn.xitu.io/2019/3/8/169595a564c1e714?w=435&h=262&f=png&s=26747

Figure 5:更改值1對依賴關係樹的影響。虛線表示將被標記爲舊的觀察者。數字表示計算的順序。

衍生將自動對狀態變化作出反應。全部反應同步發生,更重要的是無瑕疵。修改可觀察值時,將執行如下算法:

  1. 可觀察值向全部觀察者發送過期通知,代表它已變得陳舊。任何受影響的computed values將以遞歸方式將通知傳遞給其觀察者。所以,依賴關係樹的一部分將被標記爲陳舊。以Figure 5爲例,當值1改變時觀察者將變成陳舊的,並用橘色虛線標記。全部的衍生均可能被變化的值影響。

  2. 在發送陳舊通知而且存儲新值以後,一個就緒通知將被髮送。用於指示該值是否確實發生了變化。

  3. 一旦衍生收到步驟1中收到的每一個陳舊通知的就緒通知,它就會知道全部的被觀察值都穩定了,因而將開始從新計算。計算 就緒/陳舊 消息的數量將確保這一點。例如,計算值4將僅在計算值3變得穩定後從新計算。

  4. 若是沒有就緒消息指出一個值變化了,衍生將直接告訴本身的觀察者它已經準備好了且沒有變化中的值,不然將從新計算併發送一個就緒消息給本身的觀察者。執行順序如Figure 5所示。注意,若是計算值4從新評估但沒有產生新值,則最後一個「-」將永遠不會執行。

前兩段總結了如何在運行時跟蹤可觀察值和衍生之間的依賴關係以及變化在衍生中是如何傳播的。此時你會發現reaction基本上就是一個始終處於反應模式的computed value。**重點:這個算法能夠很是有效地實現而不須要閉包,只須要一堆指針數組。**另外,Mobx還應用了許多其餘優化,這些優化超出了本文的範圍。

同步執行

人們常驚訝於Mobx同步運行全部內容。這有兩大好處:第一點是不可能觀察陳舊的衍生。所以,在更改影響它的值後,能夠當即使用衍生值。第二點是這讓追蹤堆棧和調試變得簡單,它避免了Promise/Async庫所特有的無用堆棧跟蹤。

transaction(() => {
  michel.firstName = "Mich";
  michel.lastName = "W.";
});
複製代碼

(事務示例,它確保沒有人能追蹤到像Michaaa這樣的中間值)

同步執行還引入了對事務的需求。若是當即連續應用幾個突變,在應用全部更改後,最好從新評估全部衍生。在transaction中包裝action能實現這個目的。事務推遲全部就緒通知,直到事務塊執行完成。請注意,事務仍然同步運行和更新全部內容。

這總結了Mobx最基本的實現細節。這沒有涵蓋全部內容,可是很高興你能夠組合你的computed value了。經過組合reactive computations,甚至能夠自動的將一張數據圖轉換爲另外一張數據圖並用最少的補丁數保持最新的衍生,這使得實現複雜模式變得簡單。

總結

  1. 複雜應用程序的狀態最好用圖表表示,以實現參考一致性,更接近問題核心的心理模型。

  2. 不該該使用手動定義的訂閱或遊標來強制更改狀態,這將不可避免的致使因爲訂閱不足或超額訂閱致使的錯誤。

  3. 使用運行時分析來肯定最小的observer->observable的關係,這致使了一種計算模型,能夠保證在沒有觀察過時值的狀況下運行最小量的衍生。

  4. 任何不須要實現有效反作用的衍生均可以徹底優化。

原文地址: hackernoon.com/becoming-fu…

相關文章
相關標籤/搜索