本文由 MobX 做者 Michel Weststrate 所著,原文: https://hackernoon.com/the-fundamental-principles-behind-mobx-7a725f71f3e8javascript
不久以前 Bertalan Miklos 寫了一篇很好的博文,比較了 MobX 和基於 proxy 的 NX-framework。這篇博文不只證實了 proxy 的可行性,更好之處在於其觸及了 MobX 中一些很是基礎但一般又被隱藏的概念。迄今爲止我還還沒有詳細闡述過這些概念,因此本文將分享一些 MobX 特性背後的心路歷程。java
那篇文章觸及了 MobX 一個很是顯著的特性(恕我直言):在 MobX 中,全部派生(derivation)都是同步運行的。這十分不尋常,由於若是也有派生,大部分 UI 框架並不這樣作(像 RxJS 那種反應式/流式的庫默認也是同步運行的,但它們缺乏透明的跟蹤,因此這種情形不徹底有可比性)。react
在開發 MobX 以前,我花了好些個工夫研究開發者如何看待現有的庫。像 Meteor、Knockout、Angular、Ember 和 Vue 這樣的框架都顯露了與 MobX 相似的反應式行爲,且都已經存在好久了。那爲何我要創建 MobX 呢?當翻遍了人們關於這些庫的不滿 issues 和評論後,我發現了一個重複出現的主題,形成了對反應式的預期和實踐中不得不該對的糟糕問題之間的分歧。編程
那個頻現的主題就是「可預測性」。若是框架運行了你的代碼兩次,或者延遲一下再運行,就變得難以調試了。或者可能的緣由是,即使如 Promise 這樣「簡單的」抽象,也由於其自然的異步性而衆所周知的難以調試。後端
我接受不可預測性的存在,挺正常的,對於 Flux 模式特別是 Redux 來講之因此流行的最重要的緣由之一即是:它精確處理了規模變大時的可預測性問題,除此以外並沒有任何神奇之處。數組
MobX 則另闢蹊徑;與停留在整個自動化追蹤並運行函數的概念背後不一樣的是,嘗試去定位根本的問題,以便咱們始終能從這種模式中收益。透明的反應式是聲明式、高階和簡潔的。爲此增長了兩個約束:瀏覽器
約束1:所謂的 「雙執行」。 確保若是一個派生值依賴於另外一個派生值的時候,這些派生以正確的順序進行,以杜絕其中任何一個偶然讀取到過期的值。這種機制如何運行的細節在此前一篇 博文 中描述過。緩存
約束2:派生不能陳舊,就更有意思一些。不僅是其提供了所謂 「glitches」 (暫時的不一致),還由於其引入了一種不一樣的調度派生的基礎手段。安全
迄今爲止的 UI 庫每每採用省事的辦法調度派生:給派生作髒標記,並在全部狀態都被更新後的下一個 tick 再次運行之。網絡
這樣簡單又粗暴。若是隻考慮更新 DOM,這是種不錯的方法。DOM 老是有點「遲鈍」,難以程序性的讀取其數據,因此暫時的陳舊不是個事。然而暫時性陳舊會破壞反應式庫的適用性。如下面的代碼爲例:
const user = observable({
firstName: 「Michel」,
lastName: 「Weststrate」,
// MobX computed attribute
fullName: computed(function() {
return this.firstName + " " + this.lastName
})
})
user.lastName = 「Vaillant」
sendLetterToUser(user)
複製代碼
當前有趣的問題在於:當 sendLetterToUser(user) 運行時,它會獲得更新後的仍是陳舊版本的 fullName 呢?在 MobX 中答案永遠是「更新過的」:由於 MobX 保證了任何派生都是同步的。這不只避免了一些意外,同時由於派生老是有在其執行棧內引發的突變,使得調試也更簡單了。
因此若是你對爲何一個派生會運行抱有疑問,只要回溯執行棧找到引起派生無效的 action 便可。若是 MobX 對派生使用了異步調度/執行,則這些優勢就不存在了,這個庫也就不會像如今同樣廣泛適用了。
當我啓動 MobX 項目時,要達到對派生樹排序並對每一個突變運行派生,存在大量是否充分可行的懷疑。
但正如咱們如今所見,藉助於這個系統,比手工優化代碼有效得多。
應該稍稍花費精力的是,突變應該被打包在事務中,以使得多個改變的執行是原子性的。派生的執行被推遲到事務結束時,但依然是同步執行了它們。更酷的是,若是在事務結束以前使用了一個計算值,MobX 將會保證你獲得一個更新後的值!
實際上幾乎沒人明確的使用事務,在 MobX 3 中,事務甚至被棄用了。由於 action 自動應用了事務。action 在概念上更優雅了;一個 action 表示了一個用來更新狀態的函數。而 reaction 正相反,被用來響應狀態的改變。
actions、state、computed values 和 reactions 之間的概念關係
MobX 強烈聚焦的另外一件事,是能夠被推導的值(計算值)之間的分離,以及若是狀態改變後(reactions)應該被自動觸發的反作用。這些概念的分離是 MobX 很是重要的基礎。
一個派生的例子:藍色爲可觀察的狀態,綠色爲計算值,紅色爲 reactions。 淺綠色表示,若是計算值未被 reaction 觀察(間接的),就會被延遲。MobX 確保在突變以後,每一個派生只以最優的順序執行一次。
計算值應該老是優於 reactions
緣由有這麼幾個:
它們在概念上提供了很大的清晰度。計算值應該老是單純的依據其餘可觀察的值表示。這致使了一個乾淨的計算派生圖,好過一個不清晰的互相觸發的 reactions 鏈。
換句話說,reaction 觸發更多 reactions,或者 reactions 更新狀態:在 MobX 中這些都被認爲是反模式的。鏈式 reactions 將致使一個難以跟蹤的事件鏈,應該杜絕。
對於計算值,MobX 能夠感知它們是否在某處被使用。這意味着計算值能夠被自動延遲並被垃圾回收。這節省了大量的引用,並對性能有顯著的積極影響。
計算值被強制執行爲無反作用的。由於其不被容許有反作用,MobX 就能夠安全的對其執行前後從新排序,以保證從新運行次數的最小化。能夠簡單的認爲,若是計算值未被觀察,就懶運行其計算。
計算值會被自動緩存。這意味着讀取一個計算值時,只要相關的可觀察屬性不變,就不會從新運行計算。
話說回來,每一個軟件系統都須要反作用,例如發起網絡請求或刷新 DOM。所以咱們老是須要將反應式帶到命令式代碼中去,不過藉助 React 觀察者組件這類乾淨的抽象能夠很好的封裝此類 reactions。
因此 MobX 拿捏了很好的分寸,以確保陳舊值不會被觀察,且派生不會超過預期的頻繁運行。事實上,若是沒有活躍的監聽,計算壓根不會運行。實踐中可能有所區別,對於 MobX 存在一些初始的阻力,由於人們習慣於 MVVM 框架的不可預測性。可是,語義清晰的 actions、計算值和 reactions,沒有陳舊值能夠被觀察,全部派生運行在同一個棧中 -- 我相信這些事實將對一切作出改變。
MobX 被普遍用於產品中,所以要承諾能在每種 ES5 環境中運行。這使得在實際瀏覽器中使用 MobX 成爲可能,但也使得在此時支持 Proxy 沒法實現。基於這個緣由,MobX 有一些不完善之處,好比不徹底支持 可擴展對象的動態屬性(Expando properties) 而且使用了 類數組元素(faux-arrays)。一直計劃最終遷移到基於 Proxy 的實現也不是個祕密了。MobX 3 已經有一些爲使用 Proxy 作出的改變了,首個可選的基於 Proxy 的特性指日可待。但核心部分將保持非 Proxy,直到絕大多數設備和瀏覽器支持它。
無論之後是否要遷移到 Proxy 的實現, modifiers / shallow observable 這些概念都會以某種形式保留在 MobX 中。
保留 modifiers(譯註:即 observable.deep、observable.ref、observable.shallow、observable.struct 這些修飾符) 機制的緣由並不是考慮性能,而是互操做性。
當應用狀態中的全部數據都在控制中的時候,自動可觀察性是很是方便的,MobX 也是基於此開始開發的。但有時你會發現世界不如你指望的那麼理想。每一個應用中都有若干個庫,每一個庫按本身的規則行事、執行本身的設想。modifiers 和 shallow collections 被 MobX 引入,以便清晰的區分哪些數據能夠被 MobX 管理。
好比,有時須要存儲對外部概念的引用。可是,將外部庫管理的對象(如 JSX 或 DOM 元素)自動轉換爲可觀察對象常常是不符合指望的,這很容易將內部假設引入外部庫。能夠輕易的在 MobX 問題追蹤器中找出一些無心間將對象轉爲可觀察對象引發的非預期行爲的問題。modifiers 不是「儘快把這個弄好」的意思,而是表示「只觀察對象的引用,將對象自己視爲超出控制的黑盒子」。
這種概念在處理不可變數據類型的時候也很是合適。一個可行的例子是,建立一個可觀察的消息 map,消息自己是不可變數據結構的。
第二個問題是自動可觀察集合老是建立「克隆」,這並不老是能夠接受的。Proxy 老是產生一個新對象,並只以「一個方向」工做。若是由最初的庫改變了一個 proxy 對象的原始對象值,則 proxy 沒法知道這個改變。以下:
const target = { x: 3 }
const proxy = createObservableProxy(target)
observe(() => {
console.log(proxy.x)
})
target.x = 4
// proxy.x 如今是 4, 可是沒有log被打印,就是說 proxy 的 setter 沒有被調用!
複製代碼
modifiers 提供了應對這些情形的必要靈活性。由於 MobX 當前使用屬性描述符(property descriptors),也就能實際的影響既有對象,因此的確須要的話,數據突變能夠雙向工做。
也就是說,NX 在讀取期間即時生成可觀察 proxy 的方式超級有趣。我還不太肯定它是如何處理引用透明性的,但目前看上去作的很是聰明。藉助讀寫 $row 避免 modifiers 是很是有趣的作法。我拿不許這樣是否能清楚的讀寫,但無疑這省去了介紹相似 shallow 這些概念時的成本。
關於 untracked 的語義有一個必要的小提示,就是不像推斷那樣,它和 NX 的升級 $row 並不相干。在 MobX 中不通知觀察者就沒法升級數據,也會引入在應用中存在過時數據的可能性,這就違背了 MobX 的理念。人們有時但願有這種機制,但我還沒遇到過概念上沒法解決的實際用例。
untracked 反其道而行之:不關心沒法探測的寫操做,而是隻將讀操做變爲不跟蹤的。換句話說,這種方式意味着咱們絕不關心所用數據在將來的更新。和 transaction 同樣,不多在實際中用這個 API,可是這種 action 中的處理機制在概念上很是有意義:action 運行以響應用戶事件,而非狀態改變,因此它們不該跟蹤其使用的數據 -- 那些事是 reaction 要作的。
MobX 被設計爲一種通用應用反應式庫,而不僅是用來從新渲染 UI 的工具集。
相反,它推廣了一種有效工做(兼具性能和效果)的概念,那就是數據應該儘可能由其餘數據推斷出來。MobX 用在後端進程中也遊刃有餘。同步運行推斷,以及將計算值和 reaction 分離開來是 MobX 的基礎,這引導了應用狀態解構變得更清晰。
最後,nx-observe 證實了 proxy 是透明反應式編程庫很是可行的基礎,概念上和性能上都是如此。
長按二維碼或搜索 fewelife 關注咱們哦