JavaScript實現簡單的雙向綁定

不少的前端框架都支持數據雙向綁定了,最近正好在看雙向綁定的實現,就用Javascript寫了幾個簡單的例子。前端

幾個例子中嘗試使用了下面的方式實現雙向綁定:git

  1. 發佈/訂閱模式
  2. 屬性劫持
  3. 髒數據檢測

發佈/訂閱模式

實現數據雙向綁定最直接的方式就是使用PubSub模式:github

  • 當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」,相應的界面改動會更新Modelui

// 訂閱ui-update-event事件, 將UI的變化對應的更新Model對象
Pubsub.subscrib('ui-update-event', function(fullPropName, propValue) {
    var propPathArr = fullPropName.split('.');
    self.updateModelData(propPathArr[1], propValue);
});

有了這些代碼,一個簡單的雙向綁定例子就能夠運行起來了:this

  1. 初始狀態

  2. UI變化,Model會更新,綁定在Model上的其餘控件也被更新

  3. 經過"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

總結

到此,三個例子就介紹完了,例子很簡單,但願對理解雙向綁定的實現有一些幫助。

相關文章
相關標籤/搜索