不少的前端框架都支持數據雙向綁定了,最近正好在看雙向綁定的實現,就用Javascript寫了幾個簡單的例子。前端
幾個例子中嘗試使用了下面的方式實現雙向綁定:git
實現數據雙向綁定最直接的方式就是使用PubSub模式:github
根據這個思路,能夠定義'ui-update-event'和'model-update-event'兩個事件,而後針對Model和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-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」,相應的界面改動會更新Modelui
// 訂閱ui-update-event事件, 將UI的變化對應的更新Model對象 Pubsub.subscrib('ui-update-event', function(fullPropName, propValue) { var propPathArr = fullPropName.split('.'); self.updateModelData(propPathArr[1], propValue); });
有了這些代碼,一個簡單的雙向綁定例子就能夠運行起來了:this
初始狀態
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。
到此,三個例子就介紹完了,例子很簡單,但願對理解雙向綁定的實現有一些幫助。