Mvvm 前端資料流框架精講

easyhappy發表於2018-04-01

原文連結, 如果感興趣可以加QQ群: 157937068, 一起交流。

本次分享是帶大家瞭解什麼是 mvvm,mvvm 的原理,以及近幾年產生了哪些演變。

同時借 mvvm 這個話題擴充到對各類前端資料流方案的思考,形成對前端資料流整體認知,幫助大家在團隊中更好的做技術選型。

Mvvm 的概念與發展

Mvvm & 單向資料流

Mvvm 是指雙向資料流,即 View-Model 之間的雙向通訊,由 ViewModel 作橋接。如下圖所示:

image

而單向資料流則去除了 View -> Model 這一步,需要由使用者手動繫結。

生態 - 內建 & 解耦

許多前端框架都內建了 Mvvm 功能,比如 Knockout、Angular、Ember、Avalon、Vue、San 等等。

而就像 Redux 一樣,Mvvm 框架中也出現了許多與框架解耦的庫,比如 Mobx、Immer、Dob 等,這些庫需要一箇中間層與框架銜接,比如 mobx-react、redux-box、dob-react。解耦讓框架更專注 View 層,實現了庫與框架靈活搭配的能力。

解耦的資料流框架也詮釋了更高抽象級別的 Mvvm 架構,即:View - 前端框架,Model - (mobx, dob),ViewModel - (mobx-react, dob-react)。

同時也實現了資料與框架分離,便於測試與維護。比如下面的例子,左邊是框架無關的純資料/資料操作定義,右邊是 View + ViewModel:

image

執行效率 - 髒檢測 & getter/setter 劫持

Angluar 早期的髒檢測機制雖然開創了 mvvm 先河,但監聽效率比較低,需要 N + 1 次確認資料是否有聯動變化,就像下圖所示:

image

現在幾乎所有框架都改為 getter/setter 劫持實現監聽,任何資料的變化都可以在一個事件迴圈週期內完成:

image

語法 - 特殊語法 & 原生語法

早期一些 Mvvm 框架需要手動觸發檢視重新整理,現在這種做法幾乎都被原生賦值語句取代。

資料變更方式 - Mutable & Immutable

下圖的程式碼語法雖為 mutable,但產生的結果可能是 mutable,也可能是 immutable,取決於 mvvm 框架內建實現機制:

image

Connect 的兩種寫法

由於 mvvm 支援了 mutable 與 immutable 兩種寫法,所以對於 mutable 的底層,我們使用左圖的 connect 語法,對於 immutable 的底層,需要使用右圖的 conenct 語法:

[圖片上傳失敗...(image-b7408b-1522595335875)]

對左圖而言,由於 mutable 驅動,所有資料改動會自動呼叫檢視重新整理,因此不但更新可以一步到位,而且可以資料全量注入,因為沒用到的變數不會導致額外渲染。

對右圖,由於 immutable 驅動,本身並沒有主動驅動檢視重新整理能力,所以當右下角節點變更時,會在整條鏈路產生新的物件,通過 view 更新機制一層層傳導到要更新的檢視。

從 TFRP 到 mvvm

講到 mvvm 的原理,先從 TFRP 說起,詳細可以參考 dob-框架實現 這裡以 dob 框架為例子,一步步介紹瞭如何實現 mvvm。本文簡單做個介紹。

autorun & reaction

autorun 是 TFRP 的函式效果,即整合了依賴收集與監聽,autorun 背後由 reaction 實現。

image

reaction 實現 autorun

如下圖所示,autorun 是 subscription 套上 track 的 reaction,並且初始化時主動 dispatch,從入口(subscription)處啟用迴圈,完成 subscription -> track -> 監聽修改 -> subscription 完成閉環。

image

track 的實現

每個 track 在其執行期間會監聽 callback 的 getter 事件,並將 target 與 properityKey 儲存在二維 Map 中,當任何 getter 觸發後,從這個二維表中查詢依賴關係,即可找到對應的 callback 並執行。

image

View-Model 的實現

由於 autorun 與 view 的 render 函式很像,我們在 render 函式初始化執行時,使其包裹在 autorun 環境中,第 2 次 render 開始遍剝離外層的 autorun,保證只繫結一遍資料。

這樣 view 層在原本 props 更新機制的基礎上,增加了 autorun 的功能,實現修改任何資料自動更新對應 view 的效果。

image

Mvvm 的缺點與解法?

Mvvm 所有已知缺點幾乎都有了解決方案。

無法監聽新增屬性

用過 Mobx 的同學都知道,給 store 新增一個不存在的屬性,需要使用 extendObservable 這個方法。這個問題在 Dob 與 Mobx4.0 中都得到了解決,解決方法就是使用 proxy 替代 Object.defineProperty

image

非同步問題

由於 getter/setter 無法獲得當前執行函式,只能通過全域性變數方式解決,因此 autorun 的 callback 函式不支援非同步:

image

巢狀問題

由於 reaction 特性,只支援同步 callback 函式,因此 autorun 發生巢狀時,很可能會打亂依賴繫結的順序。解決方案是將巢狀的 autorun 放到執行佇列尾部,如下圖所示:

image

無資料快照

mutable 最被人詬病的一點就是無法做資料快照,不能像 redux 一樣做時間回溯。有問題自然有人會解決,Mobx 作者的 Immer 庫完美的解決了問題。

image

原理是通過 proxy 返回代理物件,在內部通過淺拷貝替代對物件的 mutable 更改。具體原理可以參考我的這篇文章:精讀 Immer.js 原始碼

image

無副作用隔離

mvvm 函式的 Action 由於支援非同步,許多人會在 Action 中發請求,同時修改 store,這樣就無法將請求副作用隔離到 store 之外。同時對 store 的 mutable 修改,本身也是一種副作用。

image

雖然可以將請求函式拆分到另一個 Action 中,但人為因素無法完全避免。

自從有了 Immer.js 之後,至少從支援超程式設計的角度來看,mutable 並不一定會產生副作用,它可以是零副作用的:

function inc(obj) {
  return produce(obj => obj.count++)
}
複製程式碼

上面這種看似 mutable 的寫法其實是零副作用的純函式,和下面寫法等價:

function inc(obj) {
  return {
    count: obj.count + 1,
    ...obj
  }
}
複製程式碼

而對副作用的隔離,也可以做出類似 dva 的封裝:

image

Mvvm store 組織形式

Mvvm 在專案中 stores 程式碼結構也千變萬化,這裡列出 4 種常見形式。

物件形式,代表框架 – mobx

mobx 開創了最基本的 mvvm store 組織形式,基本也是各內建 mvvm 框架的 store 組織形式。

image

Class + 注入,代表框架 – dob

dob 在 store 組織形式下了不少功夫,通過依賴注入增強了 store 之間的關聯,實現 stores -> action 多對一的網狀結構。

image

資料結構化,代表框架 – mobx-state-tree

mobx-state-tree 是典型結構化 store 組織的代表,這種組織形式適合一體化 app 開發,比如很多頁面之間細粒度資料需要聯動。

image

約定與整合,代表框架 – 類 dva

類 dva 是一種整合模式,是針對 redux 複雜的樣板程式碼,思考形成的簡化方案,自然整合與約定是簡化的方向。

另外這種方案更像一層資料 dsl,得益於此,同一套程式碼可以擁有不同的底層實現。

image

Mvvm vs Reactive programming

Mvvm 與 Reactive programming 都擁有 observable 特性,通過下面兩張圖可以輕鬆區分:

image

上面紅線是 mvvm 的 observable 部分,這裡指的是資料變化的 autorun 動作。

image

上面紅線是 Reactive programming 的 observable 部分,指的是資料來源派發流的過程。

Mvvm 與 Reactive programming 的結合

既然 redux 可以與 rxjs 結合(redux-observable),那麼 mvvm 應該也可以如此。

下面是這種方案的構想:

image

rxjs 僅用來隔離副作用與資料處理,mvvm 擁有修改 store 的能力,並且精準更新使用的 View。

總結

根據業務場景指定資料流方案,資料流方案沒有銀彈,只有貼著場景走,才能找到最合適的方案。

瞭解到 mvvm 的發展與演進,讓不同資料流方案組合,你會發現,資料流方案還有很多。

相關文章