原文鏈接: http://mp.weixin.qq.com/s/aPYSmkh-IU12lMVZ8-Vdjgjavascript
數據綁定之謎
2017-03-06 野狗
所謂的雙向綁定,無非是從界面的操做能實時反映到數據,數據的變動能實時展示到界面。html
數據綁定換種說法,若是咱們有一個 user 對象和一個 name 屬性,一旦咱們賦了一個新值給 user.name,在 UI 上就會顯示新的姓名了。前端
一樣地,若是 UI 包含了一個輸入用戶姓名的輸入框,輸入一個新值就應該會使 user 對象的 name 屬性作出相應的改變。vue
不少熱門的 JS 框架客戶端如 Ember.js,Angular.js 或者 KnockoutJS、Vue.js 等,都在最新特性上刊登了雙向數據綁定。java
這並不意味着從零實現它很難,也不是說須要這些功能的時候,採用這些框架是惟一的選擇。node
目前幾種主流的 MVC (VM) 框架都實現了雙向數據綁定,而咱們能夠把它簡單理解成是在單向綁定的基礎上給可輸入元素(input、textarea 等)添加了 change ( input ) 事件,來動態修改 Model 和 View,並無多高深;因此無需太過介懷是實現的單向或雙向綁定。( 混亂的前端界,動不動就玩捆綁 )express
實現雙向數據綁定的作法有大體以下幾種:數組
1. 發佈者-訂閱者模式(Backbone.js)瀏覽器
通常經過 sub, pub 的方式實現數據和視圖的綁定監聽數據結構
2. 髒值檢查(Angular.js)
Angular.js 經過髒值檢測的方式比對數據是否有變動,來決定是否更新視圖,最簡單的方式就是經過 setInterval() 定時輪詢檢測數據變更,固然 Google 不會這麼 low,Angular 只有在指定的事件觸發時進入髒值檢測,大體以下:
DOM 事件,譬如用戶輸入文本,點擊按鈕等。( ng-click )
XHR 響應事件 ( $http )
瀏覽器 Location 變動事件 ( $location )
Timer 事件( $timeout , $interval )
執行 $digest() 或 $apply()
3. 數據劫持(Vue.js)
vue.js 則是採用數據劫持結合發佈者-訂閱者模式的方式,經過 Object.defineProperty() 來劫持各個屬性的 setter,getter,在數據變更時發佈消息給訂閱者,觸發相應的監聽回調。
下面的想法實際上很基礎,能夠被認爲是 3 步走計劃:
咱們須要一個 UI 元素和屬性相互綁定的方法。
咱們須要監視屬性和 UI 元素的變化。
咱們須要讓全部綁定的對象和元素都能感知到變化。
本文只對目前熱度幾乎三分 Javascript 天下的三個框架進行討論。
Vue.js
Angular.js
React.js
Vue.js
我曾經在 Vue.js 的設計思想 一文中簡單剖析過 Vue.js。
基於 getter、setter 的方式
var msg = {
age:'25',
name:'Tony',
get age(){ return "30";
},
set age(x){ return this.name ="chaoxi";
}
};
msg.age = 1;
console.log(msg.name); //chaoxi
console.log(msg.age); //30
基於 defineProperty 的方式
var obj = {
a: 12
};
Object.defineProperty(obj, "x", {
get: function() { return this.a + 1
},
enumerable: true,
configurable: true,
set: function(y) {
console.log(y);
},
});
console.log(obj.x); //13
obj.x = 3; //執行set(3) 3
console.log(obj.x); //13
console.log(delete obj.x); //true for (key in obj) {
console.log(obj[key]); //12
}
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 的方式)。
上道菜,嚐嚐吧!!!
$scope.name = 'Ryan';
$scope.$watch( function( ) { return $scope.name;
}, function( newValue, oldValue ) {
console.log('$scope.name was updated!');
} );
Angular 會在 $scope 對象上註冊你的監聽函數 Listener,你能夠注意到會有日誌輸出 「$scope.name was updated!」,由於 $scope.name 由先前的 undefined 更新爲 ‘Ryan’。固然, watcher 也能夠是一個字符串,效果和上面例子中的匿名函數同樣,例如在Angular 源碼中:
if(typeof watchExp == 'string' &&get.constant){
var originalFn = watcher.fn;
watcher.fn = function(newVal, oldVal, scope) {
originalFn.call(this, newVal, oldVal, scope);
arrayRemove(array, watcher);
};
}
上面這段代碼將 watchExp 設置爲一個函數,這個函數會調用帶有給定變量名的 Listener 函數。
以插值爲例,當angular在compile編譯階段遇到這個語法元素時,內部處理邏輯以下:
walkers.expression = function( ast ){
var node = document.createTextNode("");
this.$watch(ast, function(newval){
dom.text(node, "" + (newval == null? "": "" + newval) );
})
return node;
}
這段代碼很好理解,就是當遇到插值時,會新建一個 textNode,並把值寫入到該 nodeContent 中,那麼 Angular 怎麼判斷這個節點值改變或者說新增了一個節點?
這裏就不得不提到$digest函數,首先,經過 watch 接口,會產生一個監聽隊列 $$watchers 。 $scope對象下的的 $$watchers 對象下擁有你定義的全部的 watchers。若是你進入到 $$watchers 內部,會發現它這樣的一個數組。
$$watchers = [
{
eq: false, // whether or not we are checking for objectEquality 是否須要判斷對象級別的相等
fn: function( newValue, oldValue ) {}, // this is the listener function we've provided 這是咱們提供的監聽器函數
last: 'Ryan', // the last known value for the variable$nbsp;$nbsp;變量的最新值
exp: function(){}, // this is the watchExp function we provided$nbsp;$nbsp;咱們提供的watchExp函數
get: function(){} // Angular's compiled watchExp function angualr編譯過的watchExp函數
}
];
$watch 函數會返回一個 deregisterWatch function,這意味着若是咱們使用 scope.$watch 對一個變量進行監視,那麼也能夠經過調用deregisterWatch 這個函數來中止監聽。
React.js
React 強調的是單向數據流(一直活在滿世界雙向數據綁定的皮皮蝦)。 固然,即使是單向數據流也總要有個數據的來源,若是數據來源於頁面自身上的用戶輸入,那效果也就等同於雙向綁定了;其實 React.js 有別於 Vue.js、Angular.js,大部分人覺得 React 是一個框架,確切的說,只能說它是一個用於構建用戶界面的 JS 庫。
要作到數據的單向流動,須要作到如下兩個方面。
數據狀態只保存在一處不用多說了,主要就是數據結構的設計,要避免把一種狀態用兩種描述放在不一樣的表裏,而後再來同步。這樣你再精巧的代碼都彌補不了數據結構的缺陷。數據結構比代碼重要。
狀態的讀寫操做分開,在狀態改變後通知更新 UI。
寫操做直接操做數據,不要有中間狀態,而後通知數據更新,Realm 是經過 realm.write 來處理全部的寫操做。
realm.write(() => {
let myCar = realm.create('Car', { //建立新的記錄
make: 'Honda',
model: 'Civic',
miles: 1000,
});
myCar.miles += 20; // 更新
realm.delete(myCar); //刪除
});
若是你在realm.write() 以外試圖寫操做,就會拋出錯誤,在更新後,會有一個 change event。
realm.addListener('change', () => {
//通知更新界面
})
這樣讀寫分開能夠下降程序的複雜度,使得邏輯更清晰。至於界面的更新就交給 React 了,配合得正好。
因此其實能夠考慮直接使用 Realm 來做爲 Flux 架構的 Store,而不用 Redux。
實現一個雙向數據綁定
仍是有不少方法可以實現上面的想法,有一個簡單有效的方法就是使用 PubSub 模式。
這個思路很簡單:咱們使用數據特性來爲 HTML 代碼進行綁定,全部被綁定在一塊兒的 JavaScript 對象和 DOM 元素都會訂閱一個PubSub對象。只要 JavaScript 對象或者一個HTML輸入元素監聽到數據的變化時,就會觸發綁定到 PubSub 對象上的事件,從而其餘綁定的對象和元素都會作出相應的變化。
上菜
function DataBinder( object_id ) {
// Create a simple PubSub object
var pubSub = {
callbacks: {},
on: function( msg, callback ) {
this.callbacks[ msg ] = this.callbacks[ msg ] || [];
this.callbacks[ msg ].push( callback );
},
publish: function( msg ) {
this.callbacks[ msg ] = this.callbacks[ msg ] || [] for ( var i = 0, len = this.callbacks[ msg ].length; i < len; i++ ) {
this.callbacks[ msg ][ i ].apply( this, arguments );
}
}
},
data_attr = "data-bind-" + object_id,
message = object_id + ":change",
changeHandler = function( evt ) {
var target = evt.target || evt.srcElement, // IE8 compatibility
prop_name = target.getAttribute( data_attr ); if ( prop_name && prop_name !== "" ) {
pubSub.publish( message, prop_name, target.value );
}
};
// Listen to change events and proxy to PubSub if ( document.addEventListener ) {
document.addEventListener( "change", changeHandler, false );
} else {
// IE8 uses attachEvent instead of addEventListener
document.attachEvent( "onchange", changeHandler );
}
// PubSub propagates changes to all bound elements
pubSub.on( message, function( evt, prop_name, new_val ) {
var elements = document.querySelectorAll("[" + data_attr + "=" + prop_name + "]"),
tag_name; for ( var i = 0, len = elements.length; i < len; i++ ) {
tag_name = elements[ i ].tagName.toLowerCase(); if ( tag_name === "input" || tag_name === "textarea" || tag_name === "select" ) {
elements[ i ].value = new_val;
} else {
elements[ i ].innerHTML = new_val;
}
}
}); return pubSub;
}
再次說明一下,咱們用通常的純 javascript 的少於100行的維護代碼得到了一樣的結果。
✦ ✦ ✦ ✦ ✦ ✦ ✦ ✦
原文:http://chaoxi.me/js/%E6%B5%85%E8%B0%88%E5%89%8D%E7%AB%AF/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA/2017/01/02/Data-Binding-Puzzle.html