原理
- vue.js採用數據劫持結合發佈者-訂閱者模式的方式,經過Object.defineProperty來觸發各個屬性的getter以及setter,在數據變更時發佈消息給訂閱者,並觸發相應的監聽回調。
具體步驟
- 第一步
- 初始化Vue實例,將Vue實例上綁定 dep 屬性(依賴收集)
- 調用Vue原型上的 _observe() 以及 _compile() 方法。、
- 第二步
- 經過 _observe() 方法重寫data對象的setter/getter方法,當咱們對data對象的屬性進行改變的時候,可以發佈消息給訂閱者(Watcher),觸發監聽函數(Watcher原型上的update()方法)
- 第三步
- 經過 _compile() 方法解析模板字符串,即 v-model/v-click/v-html等
- 在解析模板的同時,往dep中添加相應的監聽器。
- 在這裏操做Vue實例中的 $data
- 第四步
- 經過Watcher構造函數,收集須要監聽的元素
- 在構造函數的原型上定義 update()方法,經過數據的改變從而改變視圖。
- 最後上代碼(刪除註釋說明的話,核心代碼150行不到)
<!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>
<style>
body {
line-height: 120px;
text-align: center;
background:
color: yellow;
}
h1 {
background: red;
display: inline-block;
width: auto;
padding: 12px 24px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="app">
<form>
<input type="text" v-model="number" />
<button type="button" v-click="increment">increment</button>
</form>
<h1 v-html="number"></h1>
</div>
<script>
function Vue(options) {
this._init(options);
}
Vue.prototype._init = function(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
// 依賴收集: 對dom進行編譯解析(解析指令或模板語法)的時候收集依賴,在數據改變的時候(setter 中)進行更新。
this.dep = {};
this._observe(this.$data);
this._compile(this.$el);
};
Vue.prototype._observe = function(obj) {
var value;
var _this = this;
for (key in obj) {
if (obj.hasOwnProperty(key)) {
// 收集依賴,對全部屬性都進行一個監聽,在這裏是 number
// 在 dep 對象中添加一個 number 屬性,其值是一個數組,數組中存放的是 Watcher 實例
// 若是發現 number 發生了改變,就在 setter 中循環遍歷notice,執行 Watcher 實例的 update 方法,統一更新 number
_this.dep[key] = {
notice: []
};
value = _this.$data[key]; // 將 value 賦值爲最初是的 number 值
var dep = _this.dep[key];
Object.defineProperty(_this.$data, key, {
get() {
return value;
},
set(newVal) {
value = newVal;
dep.notice.forEach(item => {
// 這裏的item就是Watcher實例,能夠調用update()方法,通知更新
// 有幾處用到了 number 屬性,number.notice 就有幾個 Watcher 實例
// notice: {
// attr: "number",
// el: Input,
// name: "input",
// value: "value",
// vm: {...}
// }
item.update();
});
}
});
}
}
};
Vue.prototype._compile = function(root) {
//
var nodes = root.children; // [form, h1]
var _this = this;
for (var i = 0, len = nodes.length; i < len; i++) {
var node = nodes[i];
if (node.children.length) {
this._compile(node);
}
if (node.hasAttribute("v-click")) {
// 下面這種方式,有點問題,噹噹即執行函數執行完後,attrVal泄露出去了
// 致使解析 v-model 的時候,拿到的 attrVal 的值時 increment,而不是number
// 要注意
// 用這種方式也能夠實現,那麼在解析'v-model'的時候,須要將當前 (解析'v-model') if語句中var出來的attrVal傳入到當即執行函數中去
// 或者咱們統一使用ES6中的 let 來聲明 attrVal 變量。
// var attrVal = node.getAttribute('v-click');
// node.addEventListener('click', (function () {
// return _this.$methods[attrVal].bind(_this.$data);
// })())
// 這種方式就是噹噹即執行函數被銷燬以後,var出來的attrVal不會泄露出來,污染別的變量,可是能夠經過閉包能夠訪問獲得。
node.onclick = (function() {
var attrVal = node.getAttribute("v-click");
// 注意:methods方法裏面用的 this,指的是 options 裏面的 data,因此須要將方法的上下文半綁定爲 data
return _this.$methods[attrVal].bind(_this.$data);
})();
}
if (node.hasAttribute("v-model") && node.tagName === "INPUT") {
var attrVal = node.getAttribute("v-model");
node.addEventListener(
"input",
(function(i) {
// 由於 input 用到了 number,因此須要將 dep.number.notice 中添加 Watcher 實例,
// 在 number 改變時,input 的值就須要改變
_this.dep[attrVal].notice.push(
new Watcher("input", node, _this, attrVal, "value")
);
return function() {
// 當咱們在 input 裏面輸入數據的時候,就會觸發 number 的 setter 屬性
_this.$data[attrVal] = nodes[i].value;
};
})(i)
);
}
if (node.hasAttribute("v-html")) {
var attrVal = node.getAttribute("v-html");
_this.dep[attrVal].notice.push(
new Watcher("h1", node, _this, attrVal, "innerHTML")
);
}
}
};
class Watcher {
constructor(name, el, vm, attr, value) {
// name: input
// el: current element
// vm
// attr: number
// value: 元素的value (innerHTML, input.value)
this.name = name;
this.el = el;
this.vm = vm;
this.attr = attr;
this.value = value;
this.update();
}
update() {
this.el[this.value] = this.vm.$data[this.attr];
}
}
window.onload = function() {
let vm = new Vue({
el: "#app",
data: {
number: 0
},
methods: {
increment() {
this.number++;
}
}
});
};
</script>
</body>
</html>
複製代碼