MVVM 雙向綁定全量版整理

隨著各大前端框架的崛起,前端技術熱詞不斷演進,我們只有知道發展的原因,才能去理解各項技術的優劣,根據應用的實際情況做出最合適的技術棧選擇。

當前前端領域的前沿特性,雙向綁定必佔一席,雙向綁定是怎麼來的?各大框架如何實現雙向綁定?我們怎樣做出選擇?本文對此作了全面整理說明。

發展背景

早期的 Web 開發主要基於 MVC 模式,MVC 即Model-View-Controller的縮寫,表示模型 - 視圖 - 控制器,一個標準的 Web 應用組成如下:

  • View 用來把數據以某種方式呈現給用戶;
  • Model 數據;
  • Controller 接收並處理來自用戶的請求,將 Model 返回給用戶。
MVVM 雙向綁定全量版整理

這種 MVC 架構模式對於簡單的應用是合適的,符合軟件架構分層的思想。但隨著 H5 的發展以及當前各種頁面複雜操作行為及數據的出現,MVC 暴露出了痛點問題:

  1. 代碼中大量調用 DOM API 時,處理繁瑣,使得代碼難以維護。
  2. 當 Model 頻繁發生變化時,開發者需要主動更新到 View;用戶操作導致 Model 發生變化時,同樣需要將數據同步到 Model 中,很難維護複雜多變的數據狀態。

MVVM 是Model-View-ViewModel的縮寫,VM 代替了 C,改變了通信方向,View 與 Model 不發生聯繫,通過 ViewModel 傳遞。可以做到 View 層用戶操作時,ViewModel 層數據同步更新,ViewModel 數據變化時,支持同步更新到 View 層,提升了數據頻繁變化時的代碼可維護性。這裡的 View 與 ViewModel 之間的雙向同步過程,我們稱之為雙向綁定。

MVVM 雙向綁定全量版整理

用一張動圖來感受一下,表單 change 產生數據變化時自動更新 ViewModel, ViewModel 因外界事件導致數據改變時會同步到 View。

MVVM 雙向綁定全量版整理

當前熱門的前端框架 Angular 和 Vue 都主打 MVVM 模式,建立視圖層與視圖模型層之間的數據連接,可以輕鬆實現表單變化的數據反饋到模型層。而 React 框架則推薦單向數據流,使用自身 render 機制完成視圖渲染,實際上只擔任了 View 層。我們來看一看各個不同框架對此都做了怎樣的工作。

Angular 的髒值檢測

髒值檢測是 Angular 的數據更新思路,”髒值“意為dirty data,表示當前數據與上一輪 UI 更新的數據不同,Angular 通過監聽數據異步更新,採用比較不同 component 組件中方式更新 DOM。總結起來, 主要有如下幾種情況可能改變數據:

  • 用戶輸入事件,比如 click 事件;
  • 請求服務端數據 (XHR);
  • 定時事件,比如 setTimeout,setInterval。

上述三種情況都有一個共同點,即這些導致綁定值發生改變的事件都是異步發生的,如下為 JavaScript 的異步機制:

MVVM 雙向綁定全量版整理

左邊表示將要運行的代碼,這裡的 stack 表示 JavaScript 的運行棧,而 WebApi 則是瀏覽器中提供的一些 JavaScript 的 API,TaskQueue 表示 JavaScript 中任務隊列,因為 JavaScript 是單線程的,異步任務在任務隊列中執行。如果這些異步的事件在發生時能夠執行 Angular 重寫的異步事件,通知到 Angular 框架,那麼 Angular 就能及時的檢測到變化。Angular 在這裡使用了 Zone.js 做異步處理的髒值檢測,細節可以查看《Zone.js 究竟是如何工作的》以及 angular/zone.js 源碼。

Angular 的每一個 Component 都對應有一個changeDetector,意為變化檢測器。由於我們的多個 Component 是一個樹狀結構的組織,一個 Component 對應一個 changeDetector,所以 changeDetector 之間同樣是一個樹狀結構的組織。

我們用一個圖例來進行說明檢測到髒值變化後的更新過程,下圖每一個模塊都是一個 changeDetector 變化檢測器,紅色區塊表示有 UI 更新的變化檢測器,左側為髒值變化檢測前,右側為有髒值變化時的 UI 更新。根據上述 EventLoop 機制,Angular 框架層捕獲到異步事件對一個 component 數據更新後,從根組件開始,通知當前組件鏈路及以下鏈路,進行 View 層更新 (OnPush 模式)。因此,無論是從 V 層出發的用戶輸入事件,還是 VM 處產生的定時、Http 事件,都能保證數據更新、界面更新!

MVVM 雙向綁定全量版整理

MVVM 雙向綁定全量版整理

但是自 Angular1 推出髒值檢測機制以來,在性能上一直飽受質疑,原因如下:

  1. 如果有一個 ComponentA 數據修改,影響了下路 ComponentB。
  2. 下路的 componentB 數據在更新時觸發了數據調整機制,下路 Component 更新的短時間過程中,又影響了 ComponentA。那麼最終是否會形成 ComponentA、ComponentB 的檢測循環?

對於以上問題,Angular1 的解決方式是,如果有 10 次往復循環,就不再進行繼續檢測。無論是性能還是結果,都不能令人滿意。因此 Angular2 以脫胎換骨的方式進行了重構,支持 dev 開發模式與 prod 線上模式,prod 線上模式只進行單向數據流檢測,並支持設置ChangeDetectionStrategy變化檢測策略,具備局部組件 View 層更新能力,因此性能上有了明顯突破。

MVVM 雙向綁定全量版整理

Vue 的數據劫持 + 發佈訂閱

Vue 的雙向綁定策略基礎是數據劫持,在 Vue2.0 中使用了 ES5 語法 Object.defineProperty,來劫持各個屬性的 setter/getter,在數據變動時發佈消息給訂閱者(Wacther), 觸發相應的監聽回調。先來看一下這個 ES5 特性,我們可以通過 Object.defineProperty 這個方法,直接在一個對象上定義一個新的屬性,或者修改已存在的屬性,最終這個方法會返回該對象,如下為簡單說明,對該特性不瞭解的同學可以查看《JavaScript 高級程序設計》的第六章,或者在線訪問 MDN Web 文檔。

var o = {};
var value = 1;
Object.defineProperty(o, 'a', {
get: function() { return value; },
set: function(newValue) { value = newValue; },
enumerable: true,
configurable: true
});
o.a; // 1
o.a = 2;
o.a; // 2

結合這一特定與發佈訂閱機制,可以實現完整的雙向綁定。如下所示,Observer 數據監聽器能夠對數據對象的所有屬性進行監聽,如有變動可拿到最新值並通知訂閱者,內部採用 Object.defineProperty 的 getter 和 setter 來實現。

Compile 指令解析器,它的作用對每個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數。

Watcher 訂閱者, 作為連接 Observer 和 Compile 的橋樑,能夠訂閱並收到每個屬性變動的通知,執行指令綁定的相應回調函數。

Dep 消息訂閱器,內部維護了一個數組,用來收集訂閱者(Watcher),數據變動觸發 notify 函數,再調用訂閱者的 update 方法。

當執行 new Vue() 時,Vue 就進入了初始化階段,一方面會遍歷 data 選項中的屬性,用 Object.defineProperty 將它們轉為 getter/setter,實現數據變化監聽功能;另一方面,Vue 的指令編譯器 Compile 對元素節點的指令進行掃描和解析,初始化視圖,並訂閱 Watcher 來更新視圖, 此時 Wather 會將自己添加到消息訂閱器中 (Dep), 初始化完畢。當數據發生變化時,Observer 中的 setter 方法被觸發,setter 會立即調用 Dep.notify(),Dep 開始遍歷所有的訂閱者,並調用訂閱者的 update 方法,訂閱者收到通知後對視圖進行相應的更新。

MVVM 雙向綁定全量版整理

使用 Object.defineProperty 這個特性存在一些明顯的缺點,總結起來大概是下面兩個:

1、Object.defineProperty 無法監控到數組下標的變化,當監控數組數據對象的時候,實質上就是監控數組的地址,地址不變也就不會被監測到。為了解決這個問題,經過 Vue 內部處理後可以使用 push、pop、shift、unshift、splice、sort、reverse 來監聽數組。

2、Object.defineProperty 只能劫持對象的屬性, 因此我們需要對每個對象的每個屬性進行遍歷。Vue 2.x 裡,是通過 遞歸 + 遍歷 data 對象來實現對數據的監控的,如果屬性值也是對象那麼需要深度遍歷,顯然如果能劫持一個完整的對象是才是更好的選擇。

由於只針對了以上八種方法進行了 hack 處理, 所以其他數組的屬性也是檢測不到的,還是具有一定的侷限性。Vue3.0 中使用了 ES6 語法 Proxy,用於取代 defineProperty,使用 Proxy 有以下兩個優點:

1、可以劫持整個對象,並返回一個新對象;

2、有 13 種劫持操作。

既然 Proxy 能解決以上兩個問題,而且 Proxy 作為 ES6 的新屬性在 Vue2.x 之前就有了,為什麼 Vue2.x 不使用 Proxy 呢?一個很重要的原因就是,Proxy 是 ES6 提供的新特性,兼容性不好,並且這個屬性無法用 polyfill 來兼容。

Vue 的雙向綁定策略成為當前考察前端人員技術功底的重點,我們以 Object.defineProperty 特性實現一個簡單的雙向綁定,實現最初的 hello everyone 效果。




<title> 雙向綁定最最最初級 demo/<title>






<button> 更新數據 /<button>




由於Object.defineProperty默認只能劫持值類型數據,對引用類型數據的內部修改無法劫持,需要重寫覆蓋原原型方法,以 Array 為例,如下可以支持到 7 種數組方法:

let arr = [];
let arrayMethod = Object.create(Array.prototype);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
Object.defineProperty(arrayMethod, method, {
enumerable: true,
configurable: true,
value: function () {
let args = [...arguments]
Array.prototype[method].apply(this, args);
console.log(`operation: ${method}`);
}
})
});
arr.__proto__ = arrayMethod;
arr.push(1); // 劫持到了 push 方法

相對完整的仿 Vue 雙向綁定實現,來自雙向綁定數組源碼。




<title> 雙向綁定支持數組監聽 /<title>










React 的工具輔助

React 推薦單向數據流,目標從來不是“讓開發者寫更少的代碼”,而是讓“代碼結構更加清晰易於維護”。加上有如下 Redux 的狀態管理方案,可以很清楚地瞭解應用中狀態數據,體現其單向數據流的優勢。

MVVM 雙向綁定全量版整理

由於 React 推薦單向數據流,沒有上述 Angular 和 Vue 的雙向綁定特性,如果出現似表單類用戶視圖和存儲數據有同步的業務場景,我們需要怎麼實現?Redux 的重型狀態管理不適合應用於每個場景。一般在 React 裡的表單,我們可以監聽 “change” 事件來實現數據變更,默認寫法是從數據源(通常是 DOM)讀取並在我們的某個組件調用 setState() , 如下代碼為常規的表單使用方式:

var NoLink = React.createClass({
getInitialState: function() {
return {message: 'Hello!'};
},
handleChange: function(event) {
this.setState({message: event.target.value});
},
render: function() {
var message = this.state.message;
return ;
}
});

以上寫法每出現一個表單,就需要綁定一個事件,在只有少量表單時還能使用,一旦表單增多,維護大量 value 和 onChange 成了 React 的痛點。React 官方也提供了一種方案 ReactLink:設置如上代碼描述的通用數據迴流模式的語法糖,或者 “linking” 某些數據結構到 React state,做一層對 onChange 和 setState() 模式的薄包裝。它沒有根本性地改變你的 React 應用裡數據如何流動,下面是 ReactLink 提供的使用方式:

var LinkedStateMixin = require('react-addons-linked-state-mixin');
var WithLink = React.createClass({
mixins: [LinkedStateMixin],
getInitialState: function() {
return {message: 'Hello!'};
},
render: function() {
return ;
}
});

實際項目中,我們幾乎不會使用官方提供的 createClass 方案,畢竟寫法受限。如何減少 value 與 onChange 的使用,簡化 React 下的表單開發,成為了大量輪子製造工程的出發點。下述各團隊產出的 form 表單解決方案都給出了一定的方式,以及其他各種平臺下的開源輪子數不勝數,可以選擇一兩種進行了解。

  • 阿里 Fusion:Form 表單組件設計之路 - 高易用性
  • 阿里飛冰: Ice FormBinder
  • 阿里供應鏈平臺:面向複雜場景的高性能表單解決方案 - UForm
  • 阿里 NoForm:一個更好的表單解決方案
  • React 實現高度簡潔的 Form 組件
  • redux-form
  • final-form

追根究底,都是對大量的 value 與 onChange 進行整合,將表單 DOM 的使用方式簡化,由輔助函數統一控制。我們可以通過處理函數通用化,來模擬文中提到的動態雙向綁定效果。

import React, {Component} from 'react'
export default class Hello extends Component {
state = { val: '' };
handleInput = _event => {
let event = _event;
let elem = event.target;
let value = elem.value;
if (elem.attributes.bindField !== null) {
let attr = elem.attributes.bindField.value;
this.setState(state => state[attr] = value);
}
}
updateValue = (getFiled, getValue) => {
let fieldNodeList = [...document.querySelectorAll('[bindField]')];
let fieldNode = fieldNodeList.find(node => node.attributes[0].nodeValue === getFiled);
fieldNode.value = getValue;
let attr = fieldNode.attributes.bindField.value;
this.setState(state => state[attr] = getValue);

}
render() {
return (


{this.state.val}


<button> this.updateValue('val', 'hello world')}> 設置 /<button>

)
}
}

寫在最後

關於 MVVM 雙向綁定的策略百花齊放,各有利弊,Angular 與 Vue 通過不同策略直接將雙向綁定特性植入框架中,React 推薦單向數據流,但也確實存在不少需要減少編碼的雙向綁定場景,因此湧現了大量雙向綁定輔助庫。

Angular 框架大而全,主要由 Google 團隊維護,從特性上看適用於中後臺應用,每個關鍵模塊都有官方主導,因此更為穩定。React 實際上僅為一個視圖層 Js 庫,由 Facebook 推出,但經過社區整合,已經形成了完整的生態。Vue 屬於後起之秀,沒有大廠背景依靠,但由於使用簡單,渲染快,在國內市場增速明顯,周邊生態也已成熟。

無論技術如何實現,只有適合自己當前業務場景的策略才是最好的解決方案。

作者介紹:
飛來,就職於阿里巴巴 CBU 體驗技術部,一個以前常寫 Angular,剛開始轉 React,研究過但沒真正寫 Vue 的前端人。


分享到:


相關文章: