1. 程式人生 > >雙向資料繫結原理

雙向資料繫結原理

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

一般通過sub, pub的方式實現資料和檢視的繫結監聽,更新資料方式通常做法是 vm.set(‘property’, value),雖然老套古板,這種方式的優點在於相容ie8以下版本。

2. 髒值檢查(angular.js)

angular.js 是通過髒值檢測的方式比對資料是否有變更,來決定是否更新檢視,簡單來說就是通過在指定的事件觸發時利用 setInterval() 定時輪詢檢測資料變動。

事件的觸發情況如下:

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

3. 資料劫持(vue.js)

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

Object.defineProperty(obj, "key", {
  // 可否列舉
  enumerable: false,
  // 可否刪除
  configurable: false
, // 可否修改 writable: false, // 屬性的值 value: "static"set: function() { return a;}, get: function(newVal) { return newVal } });

若想實現一個mvvm的雙向資料繫結,需要注意一下幾點:

  1. 實現一個數據監聽器Observer,能夠對資料物件的所有屬性進行監聽,如有變動可拿到最新值並通知訂閱者
  2. 實現一個指令解析器Compile,對每個元素節點的指令進行掃描和解析,根據指令模板替換資料,以及繫結相應的更新函式
  3. 實現一個Watcher,作為連線Observer和Compile的橋樑,能夠訂閱並收到每個屬性變動的通知,執行指令繫結的相應回撥函式,從而更新檢視
  4. mvvm入口函式,整合以上三者

    這裡寫圖片描述

Observer:

        function observe(data) {
            if(!data || typeof data !== 'object') {
                return;
            }
            // 取出所有屬性遍歷
            Object.keys(data).forEach(function(key) {
                defineReactive(data, key, data[key]);
            });
        };

        function defineReactive(data, key, val) {

            Object.defineProperty(data, key, {
                enumerable: true, // 可列舉
                configurable: false, // 不能再define
                get: function() {
                    return val;
                },
                set: function(newVal) {
                    console.log('值變化: ', val, ' --> ', newVal);
                    val = newVal;
                }
            });
        }
        var data = {
            name: 'zrz'
        };
        observe(data);

Compiler:

compile主要做的事情是解析模板指令,將模板中的變數替換成資料,然後初始化渲染頁面檢視,並將每個指令對應的節點繫結更新函式,新增監聽資料的訂閱者,一旦資料有變動,收到通知,更新檢視,如圖所示:
這裡寫圖片描述

Compile.prototype = {
    // ... 省略
    compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;   // 表示式文字
            // 按元素節點方式編譯
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }
            // 遍歷編譯子節點
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

    compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 規定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令為 v-text
            var attrName = attr.name;   // v-text
            if (me.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);    // text
                if (me.isEventDirective(dir)) {
                    // 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
            }
        });
    }
};

// 指令處理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    // ...省略
    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化檢視
        updaterFn && updaterFn(node, vm[exp]);
        // 例項化訂閱者,此操作會在對應的屬性訊息訂閱器中添加了該訂閱者watcher
        new Watcher(vm, exp, function(value, oldValue) {
            // 一旦屬性值有變化,會收到通知執行此更新函式,更新檢視
            updaterFn && updaterFn(node, value, oldValue);
        });
    }
};

// 更新函式
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    }
    // ...省略
};

Watcher:

Watcher訂閱者作為Observer和Compile之間通訊的橋樑,主要做的事情是:
1、在自身例項化時往屬性訂閱器(dep)裡面新增自己
2、自身必須有一個update()方法
3、待屬性變動dep.notice()通知時,能呼叫自身的update()方法,並觸發Compile中繫結的回撥

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    // 此處為了觸發屬性的getter,從而在dep新增自己,結合Observer更易理解
    this.value = this.get(); 
}
Watcher.prototype = {
    update: function() {
        this.run(); // 屬性值變化收到通知
    },
    run: function() {
        var value = this.get(); // 取到最新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal); // 執行Compile中繫結的回撥,更新檢視
        }
    },
    get: function() {
        Dep.target = this;  // 將當前訂閱者指向自己
        var value = this.vm[exp];   // 觸發getter,新增自己到屬性訂閱器中
        Dep.target = null;  // 新增完畢,重置
        return value;
    }
};
// 這裡再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
    get: function() {
        // 由於需要在閉包內新增watcher,所以可以在Dep定義一個全域性target屬性,暫存watcher, 新增完移除
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});
Dep.prototype = {
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update(); // 呼叫訂閱者的update方法,通知變化
        });
    }
};