很多的前端框架都支持數據雙向綁定了,最近正好在看雙向綁定的實現,就用Javascript寫了幾個簡單的例子。
幾個例子中嘗試使用了下面的方式實現雙向綁定:
- 發布/訂閱模式
- 屬性劫持
- 臟數據檢測
發布/訂閱模式
實現數據雙向綁定最直接的方式就是使用PubSub模式:
- 當model發生改變的時候,觸發Model change事件,然后通過響應的事件處理函數更新界面
- 當界面更新的時候,觸發UI change事件, 然后通過相應的事件處理函數更新Model,以及綁定在Model上的其他界面控件
根據這個思路,可以定義'ui-update-event'和'model-update-event'兩個事件,然后針對Model和UI分別進行這兩個事件訂閱和發布。
UI更新
對於所有支持雙向綁定的頁面控件,當控件的“值”發生改變的時候,就觸發'ui-update-event',然后通過事件處理函數更新Model,以及綁定在Model上的其他界面控件
處理控件“值”的改變,發布“ui-update-event”事件,(這里只處理包含“t-binding”屬性的控件):
// keyup和change事件處理函數
function pageElementEventHandler(e) {
var target = e.target || e.srcElemnt;
var fullPropName = target.getAttribute('t-binding');
if(fullPropName && fullPropName !== '') {
Pubsub.publish('ui-update-event', fullPropName, target.value);
}
}
// 在頁面上添加keyup和change的listener
if(document.addEventListener) {
document.addEventListener('keyup', pageElementEventHandler, false);
document.addEventListener('change', pageElementEventHandler, false);
} else {
document.attachEvent('onkeyup', pageElementEventHandler);
document.attachEvent('onchange', pageElementEventHandler);
}
另外,對所有包含“t-binding”屬性的控件都訂閱了“'model-update-event”,也就是當Model變化的時候會收到相應的通知:
// 訂閱model-update-event事件, 根據Model對象的變化更新相關的UI
Pubsub.subscrib('model-update-event', function(fullPropName, propValue) {
var elements = document.querySelectorAll('[t-binding="' + fullPropName + '"]');
for(var i = 0, len =elements.length; i < len; i++){
var elementType = elements[i].tagName.toLowerCase();
if(elementType === 'input' || elementType === 'textarea' || elementType === 'select') {
elements[i].value = propValue;
} else {
elements[i].innerHTML = propValue;
}
}
});
Model更新
對於Model這一層,當Model發生改變的時候,會發布“model-update-event”:
// Model對象更新方法,更新對象的同時發布model-update-event事件
'updateModelData': function(propName, propValue) {
eval(this.modelName)[propName] =propValue;
Pubsub.publish('model-update-event', this.modelName + '.' + propName, propValue);
}
另外,Model訂閱了“ui-update-event”,相應的界面改動會更新Model
// 訂閱ui-update-event事件, 將UI的變化對應的更新Model對象
Pubsub.subscrib('ui-update-event', function(fullPropName, propValue) {
var propPathArr = fullPropName.split('.');
self.updateModelData(propPathArr[1], propValue);
});
有了這些代碼,一個簡單的雙向綁定例子就可以運行起來了:
-
初始狀態
-
UI變化,Model會更新,綁定在Model上的其他控件也被更新
-
通過"updateModelData"更新Model,綁定在Model上的控件被更新
完整的代碼請參考Two-way-data-binding:PubSub。
屬性劫持
在“發布/訂閱模式”實現雙向綁定的例子中,為了保證Model的更新能夠發布“model-update-event”,對於Model對象的改變必須通過“updateModelData”方法。
也就是說,通過Javascript對象字面量直接更新對象就沒有辦法觸發雙向綁定。
Javascript中提供了“Object.defineProperty”方法,通過這個方法可以對對象的屬性進行定制。
結合“Object.defineProperty”和“發布/訂閱模式”,對Model屬性的set方法進行重定義,將“model-update-event”事件的發布直接放在Model屬性的setter中:
'defineObjProp': function(obj, propName, propValue) {
var self = this;
var _value = propValue || '';
try {
Object.defineProperty(obj, propName, {
get: function() {
return _value;
},
// 在對象屬性的setter中添加model-update-event事件發布動作
set: function(newValue) {
_value = newValue;
Pubsub.publish('model-update-event', self.modelName + '.' + propName, newValue);
},
enumerable: true,
configurable: true
});
obj[propName] = _value;
} catch (error) {
alert("Browser must be IE8+ !");
}
}
這樣,就可以使用對象字面量的方式直接對Model對象進行修改:
但是,對於IE8及以下瀏覽器仍需要使用其它方法來做hack。
完整的代碼請參考Two-way-data-binding:Hijacking。
臟數據檢測
對於AngularJS,是通過臟數據檢測來實現雙向綁定的,下面就仿照臟數據檢測來實現一個簡單的雙向綁定。
在這個例子中,作用域scope對象中會維護一個“watcher”數組,用來存放所以需要檢測的表達式,以及對應的回調處理函數。
對於所有需要檢測的對象、屬性,scope通過“watch”方法添加到“watcher”數組中:
Scope.prototype.watch = function(watchExp, callback) {
this.watchers.push({
watchExp: watchExp,
callback: callback || function() {}
});
}
當Model對象發生變化的時候,調用“digest”方法進行臟檢測,如果發現臟數據,就調用對應的回調函數進行界面的更新:
Scope.prototype.digest = function() {
var dirty;
do {
dirty = false;
for(var i = 0; i < this.watchers.length; i++) {
var newVal = this.watchers[i].watchExp(),
oldVal = this.watchers[i].last;
if(newVal !== oldVal) {
this.watchers[i].callback(newVal, oldVal);
dirty = true;
this.watchers[i].last = newVal;
}
}
} while(dirty);
}
完整的代碼請參考Two-way-data-binding:Digest。
總結
到此,三個例子就介紹完了,例子很簡單,希望對理解雙向綁定的實現有一些幫助。