原理
大家都知道,vue是個MVVM框架,能夠實現view和model的雙向繫結,不像backbone那樣,model改變需要手動去通知view更新,而vue實現的原理就是通過Object.defineProperty實現資料挾持,定義setter,然後資料改變的時候通知檢視更新。
下面是網上vue的實現原理圖:
實現效果
1、MVVM
入口檔案,在這裡對vue當中的$el、methods、$data進行初始化,呼叫observer遍歷$data的資料並進行挾持,呼叫compile遍歷$el下的所有節點,解析指令和取值操作({{}})。遍歷$data的資料,通過Object.defineProperty的getter和setter實現對$data的代理。
2、Observer
遍歷data,通過Object.defineProperty設定getter和setter,在setter知道資料發生了改變,然後通知Wacher去更新view。
3、Compile
遍歷$el下的所有節點,解析指令和取值操作等,為每個節點繫結更新函式(為什麼在compile這裡繫結呢?因為這裡剛好是遍歷的節點☺),繫結事件和method的關係,同時也新增訂閱者,當接受到檢視更新的訂閱訊息後,呼叫更新函式,實現檢視更新。同時在新增訂閱者的時候,初始化渲染檢視。
4、Watcher
Watcher作為訂閱者,充當Observer和Compile的中間橋樑,包含update方法,update方法呼叫Compile中繫結的事件更新函式,實現對檢視的初始化和更新操作。
MVVM的實現
MVVM完成初始化操作,並且呼叫observer和compile。對$data進行代理,如此便可以通過this.attribute來代理this.$data.attribute。因為一個屬性可能對應多個指令,所以需要一個_binding屬性來存放屬性對應的所有訂閱者,這樣屬性一改變,就可以取出所有的訂閱者去更新檢視。
function MVVM(options) {
// 初始化
this.$data = options.data;
this.$methods = options.methods;
this.$el = options.el;
// 儲存data的每個屬性對應的所有watcher
this._binding = {};
// 呼叫observer和compile
this._observer(options.data);
this._compile();
// this.xxx 代理this.$data.xxx
this.proxyAttribute();
}
複製程式碼
Observer的實現
Observer遍歷$data,通過Object.defineProperty的setter的挾持資料改變,監聽到資料改變後取出所有該屬性對應的訂閱者,然後通知更新函式更新檢視。
注意:這裡有迴圈,且閉包(getter和setter)裡面需要依賴迴圈項(value和key),所以用立即執行函式解決迴圈項獲取不對的問題。
MVVM.prototype._observer = function(data) {
var self = this;
for(var key in this.$data) {
if (this.$data.hasOwnProperty(key)) {
// 初始化屬性對應的訂閱者容器(陣列)
this._binding[key] = {
_directives: [],
_texts: []
};
if(typeof this.$data[key] === "object") {
return this._observer(this.$data[key]);
}
var val = data[key];
// 立即執行函式獲取正確的迴圈項
(function(value, key) {
Object.defineProperty(self.$data, key, {
enumerable: true,
configurable: true,
get: function() {
return value;
},
set(newval) {
if(newval === value) {
return;
}
value = newval;
// 監聽到資料改變後取出所有該屬性對應的訂閱者,通知view更新-屬性
if(self._binding[key]._directives) {
self._binding[key]._directives.forEach(function(watcher) {
watcher.update();
}, self);
}
// 監聽到資料改變後取出所有該屬性對應的訂閱者,通知view更新-文字
if(self._binding[key]._texts) {
self._binding[key]._texts.forEach(function(watcher) {
watcher.update();
}, self);
}
}
});
})(val, key);
}
}
}
複製程式碼
Compile的實現
Compile遍歷所有的節點,解析指令,為每個節點繫結更新函式,且新增訂閱者,當訂閱者通知view更新的時候,呼叫更新函式,實現對檢視的更新。
這裡同樣需要使用立即執行函式來解決閉包依賴的迴圈項問題。
還有一點需要解決的是,如果節點的innerText依賴多個屬性的話,如何做到只替換改變屬性對應的文字問題。
比如{{message}}:{{name}}已經被編譯解析成“歡迎: 鳴人”,如果message改變為“你好”,怎麼讓使得“歡迎:鳴人”改為“你好:鳴人”。
MVVM.prototype._compile = function() {
var dom = document.querySelector(this.$el);
var children = dom.children;
var self = this;
var i = 0, j = 0;
// 更新函式,但observer中model的資料改變的時候,通過Watcher的update呼叫更新函式,從而更新dom
var updater = null;
for(; i < children.length; i++) {
var node = children[i];
(function(node) {
// 解析{{}}裡面的內容
// 儲存指令原始內容,不然資料更新時無法完成替換
var text = node.innerText;
var matches = text.match(/{{([^{}]+)}}/g);
if(matches && matches.length > 0) {
// 儲存和node繫結的所有屬性
node.bindingAttributes = [];
for(j = 0; j < matches.length; j++) {
// data某個屬性
var attr = matches[j].match(/{{([^{}]+)}}/)[1];
// 將和該node繫結的data屬性儲存起來
node.bindingAttributes.push(attr);
(function(attr) {
updater = function() {
// 改變的屬性值對應的文字進行替換
var innerText = text.replace(new RegExp("{{" + attr + "}}", "g"), self.$data[attr]);
// 如果該node繫結多個屬性 eg:<div>{{title}}{{description}}</div>
for(var k = 0; k < node.bindingAttributes.length; k++) {
if(node.bindingAttributes[k] !== attr) {
// 恢復原來沒改變的屬性對應的文字
innerText = innerText.replace("{{" + node.bindingAttributes[k] + "}}", self.$data[node.bindingAttributes[k]]);
}
}
node.innerText = innerText;
}
self._binding[attr]._texts.push(new Watcher(self, attr, updater));
})(attr);
}
}
// 解析vue指令
var attributes = node.getAttributeNames();
for(j = 0; j < attributes.length; j++) {
// vue指令
var attribute = attributes[j];
// DOM attribute
var domAttr = null;
// 繫結的data屬性
var vmDataAttr = node.getAttribute(attribute);
if(/v-bind:([^=]+)/.test(attribute)) {
// 解析v-bind
domAttr = RegExp.$1;
// 更新函式
updater = function(val) {
node[domAttr] = val;
}
// data屬性繫結多個watcher
self._binding[vmDataAttr]._directives.push(
new Watcher(self, vmDataAttr, updater)
)
} else if(attribute === "v-model" && (node.tagName = 'INPUT' || node.tagName == 'TEXTAREA')) {
// 解析v-model
// 更新函式
updater = function(val) {
node.value = val;
}
// data屬性繫結多個watcher
self._binding[vmDataAttr]._directives.push(
new Watcher(self, vmDataAttr, updater)
)
// 監聽input/textarea的資料變化,同步到model去,實現雙向繫結
node.addEventListener("input", function(evt) {
var $el = evt.currentTarget;
self.$data[vmDataAttr] = $el.value;
});
} else if(/v-on:([^=]+)/.test(attribute)) {
// 解析v-on
var event = RegExp.$1;
var method = vmDataAttr;
node.addEventListener(event, function(evt) {
self.$methods[method] && self.$methods[method].call(self, evt);
});
}
}
})(node);
}
}
複製程式碼
Watcher的實現
Watcher充當訂閱者的角色,架起了Observer和Compile的橋樑,Observer監聽到資料變化後,通知Wathcer更新檢視(呼叫Wathcer的update方法),Watcher再告訴Compile去呼叫更新函式,實現dom的更新。同時頁面的初始化渲染也交給了Watcher(當然也可以放到Compile進行)。
function Watcher(vm, attr, cb) {
this.vm = vm; // viewmodel
this.attr = attr; // data的屬性,一個watcher訂閱一個data屬性
this.cb = cb; // 更新函式,在compile那邊定義
// 初始化渲染檢視
this.update();
}
Watcher.prototype.update = function() {
// 通知comile中的更新函式更新dom
this.cb(this.vm.$data[this.attr]);
}
複製程式碼
全部程式碼
git地址:github.com/VikiLee/MVV…
使用例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="view">
<div v-bind:id="id">
{{message}}:{{name}}
</div>
<input type="text" v-model="name"/>
<button v-on:click="handleClick">獲取輸入值</button>
</div>
</body>
<script src="js/MVVM.js" type="text/javascript"></script>
<script>
var vue = new MVVM({
el: "#view",
data: {
message: "歡迎光臨",
name: "鳴人",
id: "id"
},
methods: {
handleClick: function() {
alert(this.message + ":" + this.name + ", 點選確定路飛會出來");
this.name = '路飛';
}
}
})
setTimeout(function() {
vue.message = "你好";
}, 1000);
</script>
</html>
複製程式碼