資料繫結之謎

發表於2017-03-04

所謂的雙向繫結,無非是從介面的操作能實時反映到資料,資料的變更能實時展現到介面。

data-bindind-1

資料繫結換種說法,如果我們有一個 user 物件和一個 name 屬性,一旦我們賦了一個新值給 user.name,在 UI 上就會顯示新的姓名了。

同樣地,如果 UI 包含了一個輸入使用者姓名的輸入框,輸入一個新值就應該會使 user 物件的 name 屬性做出相應的改變。

很多熱門的 JS 框架客戶端如 Ember.js,Angular.js 或者 KnockoutJS、Vue.js 等,都在最新特性上刊登了雙向資料繫結。

這並不意味著從零實現它很難,也不是說需要這些功能的時候,採用這些框架是唯一的選擇。

目前幾種主流的 MVC (VM) 框架都實現了雙向資料繫結,而我們可以把它簡單理解成是在單向繫結的基礎上給可輸入元素(input、textarea 等)新增了 change ( input ) 事件,來動態修改 Model 和 View,並沒有多高深;所以無需太過介懷是實現的單向或雙向繫結。( 混亂的前端界,動不動就玩捆綁 )

bind-img

實現雙向資料繫結的做法有大致如下幾種:

釋出者-訂閱者模式(Backbone.js)

一般通過 sub, pub 的方式實現資料和檢視的繫結監聽

髒值檢查(Angular.js)

Angular.js 通過髒值檢測的方式比對資料是否有變更,來決定是否更新檢視,最簡單的方式就是通過 setInterval() 定時輪詢檢測資料變動,當然 Google 不會這麼 low,Angular 只有在指定的事件觸發時進入髒值檢測,大致如下:

  • DOM 事件,譬如使用者輸入文字,點選按鈕等。( ng-click )
  • XHR 響應事件 ( $http )
  • 瀏覽器 Location 變更事件 ( $location )
  • Timer 事件( $timeout , $interval )
  • 執行 $digest() 或 $apply()

資料劫持(Vue.js)

vue.js 則是採用資料劫持結合釋出者-訂閱者模式的方式,通過 Object.defineProperty() 來劫持各個屬性的 setter,getter,在資料變動時釋出訊息給訂閱者,觸發相應的監聽回撥。

下面的想法實際上很基礎,可以被認為是 3 步走計劃:

  1. 我們需要一個 UI 元素和屬性相互繫結的方法。
  2. 我們需要監視屬性和 UI 元素的變化。
  3. 我們需要讓所有繫結的物件和元素都能感知到變化。

本文只對目前熱度幾乎三分 Javascript 天下的三個框架進行討論。

  • Vue.js
  • Angular.js
  • React.js

Vue.js

我曾經在 Vue.js 的設計思想 一文中簡單剖析過 Vue.js。

基於 getter、setter 的方式

基於 defineProperty 的方式

Angular.js

髒檢測基本原理

眾所周知,Angular 的雙向繫結是採用“髒檢測”的方式來更新 DOM ,但是 Angular並不存在定時髒檢測(切記); Angular 對常用的 DOM 事件、XHR 事件進行了封裝,觸發時會呼叫 $digest cycle;在 $digest 流程中,Angular 將遍歷每個資料變數的 watcher,比較它的新舊值;當新舊值不同時,觸發 Listener 函式,執行相關的操作。

Angular主要通過 scopes 實現資料雙向繫結,AngularJS 的 scopes 包括以下四個主要部分:

  • digest 迴圈以及 dirty-checking(髒檢測),包括 watch,watch,digest,和$apply。
  • scope 繼承 這項機制使得我們可以建立 scope 繼承來分享資料和事件。
  • 對集合、陣列和物件的有效 dirty-checking。
  • 事件系統 on,on,emit,以及 $broadcast。

監聽一個變數何時變化,需要呼叫 $scope.$watch 函式,這個函式接受三個引數:需要檢測的值或者表示式(watchExp)、監聽函式、值變化時執行(Listener 匿名函式),是否開啟值檢測,為 true 時會檢測物件或者陣列的內部變更(即選擇以===的方式比較還是 Angular.equals 的方式)。

good-img

上道菜,嚐嚐吧!!!


Angular 會在 $scope 物件上註冊你的監聽函式 Listener,你可以注意到會有日誌輸出 “$scope.name was updated!”,因為 $scope.name 由先前的 undefined 更新為 ‘Ryan’。當然, watcher 也可以是一個字串,效果和上面例子中的匿名函式一樣,例如在Angular 原始碼中:


上面這段程式碼將 watchExp 設定為一個函式,這個函式會呼叫帶有給定變數名的 Listener 函式。

let-me-sing

以插值為例,當angular在compile編譯階段遇到這個語法元素時,內部處理邏輯如下:


這段程式碼很好理解,就是當遇到插值時,會新建一個 textNode,並把值寫入到該 nodeContent 中,那麼 Angular 怎麼判斷這個節點值改變或者說新增了一個節點?

這裡就不得不提到$digest函式,首先,通過 watch 介面,會產生一個監聽佇列 $$watchers 。 $scope物件下的的 $$watchers 物件下擁有你定義的所有的 watchers。如果你進入到 $$watchers 內部,會發現它這樣的一個陣列。


$watch 函式會返回一個 deregisterWatch function,這意味著如果我們使用 scope.$watch 對一個變數進行監視,那麼也可以通過呼叫deregisterWatch 這個函式來停止監聽。

React.js

React 強調的是單向資料流(一直活在滿世界雙向資料繫結的皮皮蝦)。 當然,即便是單向資料流也總要有個資料的來源,如果資料來源於頁面自身上的使用者輸入,那效果也就等同於雙向繫結了;其實 React.js 有別於 Vue.js、Angular.js,大部分人以為 React 是一個框架,確切的說,只能說它是一個用於構建使用者介面的 JS 庫。

pipixia-img

要做到資料的單向流動,需要做到以下兩個方面。

資料狀態只儲存在一處不用多說了,主要就是資料結構的設計,要避免把一種狀態用兩種描述放在不同的表裡,然後再來同步。這樣你再精巧的程式碼都彌補不了資料結構的缺陷。資料結構比程式碼重要。

狀態的讀寫操作分開,在狀態改變後通知更新 UI。

寫操作直接運算元據,不要有中間狀態,然後通知資料更新,Realm 是通過 realm.write 來處理所有的寫操作。

react-js-img


如果你在realm.write() 之外試圖寫操作,就會丟擲錯誤,在更新後,會有一個 change event。


這樣讀寫分開可以降低程式的複雜度,使得邏輯更清晰。至於介面的更新就交給 React 了,配合得正好。

所以其實可以考慮直接使用 Realm 來作為 Flux 架構的 Store,而不用 Redux。

實現一個雙向資料繫結

還是有很多方法能夠實現上面的想法,有一個簡單有效的方法就是使用 PubSub 模式。

let-me-talk

這個思路很簡單:我們使用資料特性來為 HTML 程式碼進行繫結,所有被繫結在一起的 JavaScript 物件和 DOM 元素都會訂閱一個PubSub物件。只要 JavaScript 物件或者一個HTML輸入元素監聽到資料的變化時,就會觸發繫結到 PubSub 物件上的事件,從而其他繫結的物件和元素都會做出相應的變化。

上菜


再次說明一下,我們用一般的純 javascript 的少於100行的維護程式碼獲得了同樣的結果。

下期再見!!!

sleep

相關文章