原理
- 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>
复制代码