來公司一年多了,接觸了angular,vue這兩種主流的MVVM框架,之前一直沒有去思考其實現的原理,但框架其實也是用基礎的js方法實現的,所以還是很有必要去了解下其原理的,在這裡簡單的總結一下。
1,幾種實現雙向繫結的方法
實現資料繫結的大致幾種做法:
- 釋出者-訂閱者模式(backbone.js)
- 髒值檢查(angular.js)
- 資料劫持(vue.js)
釋出者-訂閱者模式: 一般通過sub,pub的方式實現資料和檢視的繫結監聽,更新資料方式通常做法是 vm.set('property', value),現在用的較少,這裡不再闡述其原理了,有興趣的同學可以點選這裡
髒值檢查: ng是通過髒檢查的檢測機制來對比資料是否發生了變更,來決定是否更新檢視。那麼髒檢查是怎麼觸發的呢,大致有一下幾種情況:
- UI事件,如ng-click
- ajax請求
- timeout延遲
- 執行$apply()(非ng事件時需我們手動觸發髒檢查)
這裡我們就髒值檢查舉個小栗子:
// html
<div ng-app="app">
<div ng-controller="myController">
<span>{{count}}</span>
<button ng-click="counter=counter+1">increase</button>
</div>
</div>
// js
var app = angular.module('app', []);
app.controller('myController', function($scope) {
$scope.count = 0;
})
複製程式碼
看下這時的效果
我們發下這時通過ng-click形成的資料更改是實時響應的。來,讓我們換種實現方式:<div ng-app="app">
<div ng-controller="CounterCtrl">
<span>{{data}}</span>
<button my-click>click</button>
</div>
</div>
app.controller('CounterCtrl', function($scope) {
$scope.data = 0;
})
app.directive('myClick', function() {
return function(scope, element, attr) {
element.on('click', function() {
scope.data++;
console.log(scope.data);
})
}
})
複製程式碼
這時的效果
我們發現雖然data的值在更改,但是view卻並沒有更新,這是因為我沒有用ng事件來觸發UI事件,所以沒有執行髒檢查,這個時候我們可以手動加下$apply試下效果。資料劫持: vue.js 則是採用資料劫持結合釋出者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在資料變動時釋出訊息給訂閱者,觸發相應的監聽回撥。
2,資料劫持的程式碼實現
這裡我們用資料劫持的方式來實現一個簡易的資料繫結,我們來寫一下:
簡單的寫下html
<div id='app'>
<h3>姓名</h3>
<p>{{name}}</p>
<h3>年齡</h3>
<p>{{age}}</p>
</div>
複製程式碼
接下來準備我們要展示資料
document.addEventListener('DOMContentLoaded', function() {
let opt = {
el:'#app',
data:{
name:'準備中...',
age:30
}
}
let vm = new MVVM(opt);
setTimeout(() => {
opt.data.name = '小明';
}, 2000);
})
複製程式碼
好,到這裡準備工作都做好了,接下來是重頭戲,不多說,直接上程式碼:
class MVVM {
constructor(opt) {
this.opt = opt;
this.observe(opt.data);
let root = document.querySelector(opt.el);
this.complie(root);
}
// 繫結觀察者物件
observe(data) {
Object.keys(data).forEach(key => {
// 宣告一個觀察者物件
let obv = new Observer();
// 記錄data[key]的值,防止set的時候遞迴呼叫報錯
data['_' + key] = data[key];
Object.defineProperty(data, key, {
get() {
Observer.target && obv.addSubNode(Observer.target);
return data['_' + key];
},
set(newVal) {
obv.update(newVal);
data['_' + key] = newVal;
}
})
})
}
// 初始化頁面,遍歷 DOM,收集每一個key變化時,隨之調整的位置,以觀察者方法存放起來
complie(node) {
[].forEach.call(node.childNodes, child =>{
if(!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)){
let key = RegExp.$1.trim()
child.innerHTML = child.innerHTML.replace(new RegExp('\\{\\{\\s*'+ key +'\\s*\\}\\}', 'gm'),this.opt.data[key])
// 記錄當前的DOM節點
Observer.target = child;
// 呼叫set函式
this.opt.data[key];
Observer.target = null;
}
else if (child.firstElementChild) {
this.compile(child);
}
})
}
}
// 常規觀察者類
class Observer{
constructor() {
this.subNode = []
}
addSubNode(node){
this.subNode.push(node)
}
update(newVal){
this.subNode.forEach(node=>{
node.innerHTML = newVal
})
}
}
複製程式碼
好了至此就是所有的程式碼,我們可以把opt宣告為全域性變數,這樣我們就可以在控制檯修改相關資料,然後實時觀察修改結果。
具體的實現程式碼參照了這篇文章50行程式碼的MVVM,感受閉包的藝術,當時看到的時候有些地方比較困惑,理解了之後這裡將其貼上來,註釋上我自己的理解,僅供學習參考吧。(感覺data['_' + key] 這種記錄方式不太好,引入了無關的值)