雙向資料繫結原理
阿新 • • 發佈:2019-01-29
1. 釋出者-訂閱者模式(backbone.js)
一般通過sub, pub的方式實現資料和檢視的繫結監聽,更新資料方式通常做法是 vm.set(‘property’, value),雖然老套古板,這種方式的優點在於相容ie8以下版本。
2. 髒值檢查(angular.js)
angular.js 是通過髒值檢測的方式比對資料是否有變更,來決定是否更新檢視,簡單來說就是通過在指定的事件觸發時利用 setInterval() 定時輪詢檢測資料變動。
事件的觸發情況如下:
- DOM事件,譬如使用者輸入文字,點選按鈕等。( ng-click )
- XHR響應事件 ( $http )
- 瀏覽器Location變更事件 ( $location )
- Timer事件
( $timeout , $interval )
- 執行
$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的雙向資料繫結,需要注意一下幾點:
- 實現一個數據監聽器Observer,能夠對資料物件的所有屬性進行監聽,如有變動可拿到最新值並通知訂閱者
- 實現一個指令解析器Compile,對每個元素節點的指令進行掃描和解析,根據指令模板替換資料,以及繫結相應的更新函式
- 實現一個Watcher,作為連線Observer和Compile的橋樑,能夠訂閱並收到每個屬性變動的通知,執行指令繫結的相應回撥函式,從而更新檢視
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方法,通知變化
});
}
};