標籤:

Mobx 思想的實現原理,及與 Redux 對比

Mobx 思想的實現原理

Mobx 最關鍵的函數在於 autoRun,舉個例子,它可以達到這樣的效果:

const obj = observable({n a: 1,n b: 2n})nnautoRun(() => {n console.log(obj.a)n})nnobj.b = 3 // 什麼都沒有發生nobj.a = 2 // observe 函數的回調觸發了,控制台輸出:2n

我們發現這個函數非常智能,用到了什麼屬性,就會和這個屬性掛上鉤,從此一旦這個屬性發生了改變,就會觸發回調,通知你可以拿到新值了。沒有用到的屬性,無論你怎麼修改,它都不會觸發回調,這就是神奇的地方。

autoRun 的用途

使用 autoRun 實現 mobx-react 非常簡單,核心思想是將組件外麵包上 autoRun,這樣代碼中用到的所有屬性都會像上面 Demo 一樣,與當前組件綁定,一旦任何值發生了修改,就直接 forceUpdate,而且精確命中,效率最高。

依賴收集

autoRun 的專業名詞叫做依賴收集,也就是通過自然的使用,來收集依賴,當變數改變時,根據收集的依賴來判斷是否需要更新。

實現步驟拆解

為了兼容,Mobx 使用了 Object.defineProperty 攔截 getter 和 setter,但是無法攔截未定義的變數,為了方便,我們使用 proxy 來講解,而且可以監聽未定義的變數哦。

步驟一 存儲結構

眾所周知,事件監聽是需要預先存儲的,autoRun 也一樣,為了知道當變數修改後,哪些方法應該被觸發,我們需要一個存儲結構。

首先,我們需要存儲所有的代理對象,讓我們無論拿到原始對象,還是代理對象,都能快速的找出是否有對應的代理對象存在,這個功能用在判斷代理是否存在,是否合法,以及同一個對象不會生成兩個代理。

代碼如下:

const proxies = new WeakMap()nnfunction isObservable<T extends object>(obj: T) {n return (proxies.get(obj) === obj)n}n

重點來了,第二個要存儲的是最重要的部分,也就是所有監聽!當任何對象被改變的時候,我們需要知道它每一個 key 對應著哪些監聽(這些監聽由 autoRun 註冊),也就是,最終會存在多個對象,每個對象的每個 key 都可能與多個 autoRun 綁定,這樣在更新某個 key 時,直接觸發與其綁定的所有 autoRun 即可。

代碼如下:

const observers = new WeakMap<object, Map<PropertyKey, Set<Observer>>>()n

第三個存儲結構就是待觀察隊列,為了使同一個調用棧多次賦值僅執行一次 autoRun,所有待執行的都會放在這個隊列中,在下一時刻統一執行隊列並清空,執行的時候,當前所有 autoRun 都是在同一時刻觸發的,所以讓相同的 autoRun 不用觸發多次即可實現性能優化。

const queuedObservers = new Set()n

代碼如下:

我們還要再存儲兩個全局變數,分別是是否在隊列執行中,以及當前執行到的 autoRun。

代碼如下:

let queued = falsenlet currentObserver: Observer = nulln

步驟二 將對象加工可觀察

這一步講解的是 observable 做了哪些事,首先第一件就是,如果已經存在代理對象了,就直接返回。

代碼如下:

function observable<T extends object>(obj: T = {} as T): T {n return proxies.get(obj) || toObservable(obj)n}n

我們繼續看 toObservable 函數,它做的事情是,實例化代理,並攔截 get set 等方法。

我們先看攔截 get 的作用:先拿到當前要獲取的值 result,如果這個值在代理中存在,優先返回代理對象,否則返回 result 本身(沒有引用關係的基本類型)。

上面的邏輯只是簡單返回取值,並沒有註冊這一步,我們在 currentObserver 存在時才會給對象當前 key註冊 autoRun,並且如果結果是對象,又不存在已有的代理,就調用自身 toObservable 再遞歸一遍,所以返回的對象一定是代理。

registerObserver 函數的作用是將 targetObj -> key -> autoRun 這個鏈路關係存到 observers 對象中,當對象修改的時候,可以直接找到對應 key 的 autoRun。

那麼 currentObserver 是什麼時候賦值的呢?首先,並不是訪問到 get 就要註冊 registerObserver,必須在 autoRun 裡面的才符合要求,所以執行 autoRun 的時候就會將當前回調函數賦值給 currentObserver,保證了在 autoRun 函數內部所有監聽對象的 get 攔截器都能訪問到 currentObserver。以此類推,其他 autoRun 函數回調函數內部變數 get 攔截器中,currentObserver 也是對應的回調函數。

代碼如下:

const dynamicObject = new Proxy(obj, {n // ...n get(target, key, receiver) {n const result = Reflect.get(target, key, receiver)nn // 如果取的值是對象,優先取代理對象n const resultIsObject = typeof result === object && resultn const existProxy = resultIsObject && proxies.get(result)nn // 將監聽添加到這個 key 上n if (currentObserver) {n registerObserver(target, key)n if (resultIsObject) {n return existProxy || toObservable(result)n }n }nn return existProxy || resultn }),n // ...n})n

setter 過程中,如果對象產生了變動,就會觸發 queueObservers 函數執行回調函數,這些回調都在 getter 中定義好了,只需要把當前對象,以及修改的 key 傳過去,直接觸發對應對象,當前 key 所註冊的 autoRun 即可。

代碼如下:

const dynamicObject = new Proxy(obj, {n // ...n set(target, key, value, receiver) {n // 如果改動了 length 屬性,或者新值與舊值不同,觸發可觀察隊列任務n if (key === length || value !== Reflect.get(target, key, receiver)) {n queueObservers<T>(target, key)n }nn // 如果新值是對象,優先取原始對象n if (typeof value === object && value) {n value = value.$raw || valuen }nn return Reflect.set(target, key, value, receiver)n },n // ...n})n

沒錯,主要邏輯已經全部說完了,新對象之所以可以檢測到,是因為 proxy 的 get 會觸發,這要多謝 proxy 的強大。

可能有人問 Object.defineProperty 為什麼不行,原因很簡單,因為這個函數只能設置某個 key 的 gettersetter~。

symbol proxy reflect 這三劍客能做的事還有很多很多,這僅僅是實現 Object.observe 而已,還有更強大的功能可以挖掘。

mobx 的 proxy 完整實現版本參考 github.com/nx-js/observ 項目。

  • symbol拓展
  • reflect拓展

談談 Redux 與 Mobx 思想的適用場景

Redux 和 Mobx 都是當下比較火熱的數據流模型,一個背靠函數式,似乎成為了開源界標配,一個基於面向對象,低調的前行。

函數式 vs 面向對象

首先任何避開業務場景的技術選型都是耍流氓,我先耍一下流氓,首先函數式的優勢,比如:

  1. 無副作用,可時間回溯,適合併發。
  2. 數據流變換處理很拿手,比如 rxjs。
  3. 對於複雜數據邏輯、科學計算維的開發和維護效率更高。

當然,連原子都是由帶正電的原子核,與帶負電的電子組成的,幾乎任何事務都沒有絕對的好壞,面向對象也存在很多優勢,比如:

  1. javascript 的鴨子類型,表明它基於對象,不適合完全函數式表達。
  2. 數學思維和數據處理適合用函數式,技術是為業務服務的,而業務模型適合用面向對象。
  3. 業務開發和做研究不同,邏輯嚴謹的函數式相當完美,但別指望每個程序員都願意消耗大量腦細胞解決日常業務問題。

Redux vs Mobx

那麼具體到這兩種模型,又有一些特定的優缺點呈現出來,先談談 Redux 的優勢:

  1. 數據流流動很自然,因為任何 dispatch 都會導致廣播,需要依據對象引用是否變化來控制更新粒度。
  2. 如果充分利用時間回溯的特徵,可以增強業務的可預測性與錯誤定位能力。
  3. 時間回溯代價很高,因為每次都要更新引用,除非增加代碼複雜度,或使用 immutable。
  4. 時間回溯的另一個代價是 action 與 reducer 完全脫節,數據流過程需要自行腦補。原因是可回溯必然不能保證引用關係。
  5. 引入中間件,其實主要為了解決非同步帶來的副作用,業務邏輯或多或少參雜著 magic。
  6. 但是靈活利用中間件,可以通過約定完成許多複雜的工作。
  7. 對 typescript 支持困難。

Mobx:

  1. 數據流流動不自然,只有用到的數據才會引發綁定,局部精確更新,但免去了粒度控制煩惱。
  2. 沒有時間回溯能力,因為數據只有一份引用。
  3. 自始至終一份引用,不需要 immutable,也沒有複製對象的額外開銷。
  4. 沒有這樣的煩惱,數據流動由函數調用一氣呵成,便於調試。
  5. 業務開發不是腦力活,而是體力活,少一些 magic,多一些效率。
  6. 由於沒有 magic,所以沒有中間件機制,沒法通過 magic 加快工作效率(這裡 magic 是指 action 分發到 reducer 的過程)。
  7. 完美支持 typescript。

到底如何選擇

從目前經驗來看,我建議前端數據流不太複雜的情況,使用 Mobx,因為更加清晰,也便於維護;如果前端數據流極度複雜,建議謹慎使用 Redux,通過中間件減緩巨大業務複雜度,但還是要做到對開發人員盡量透明,如果可以建議使用 typescript 輔助。


推薦閱讀:

高性能 MobX 模式(part 2)- 響應變化
如何評價數據流管理框架 MobX ?

TAG:Redux | MobX |