vue 实现原理(二)

      本篇是继上篇vue 实现原理(一)>)的续写。将继续实现下vue中的双向绑定和 computed。

双向绑定

      把 Model 绑定到 View 的同时也将 View 绑定到了 Model 上,这样就既可以通过更新 Model 来实现 View 的自动更新,也可以通过更新 View 来实现 Model 数据的更新。我的实现思路大致是这样的:首先获取节点元素上的所有属性并且用数组存储起来,循环遍历取出 v-model 和对应的 value 值,然后把对应的值赋给输入框。接下来就需要订阅一下,每次修改,就把对应的值赋进去。当输入框输入时,也要有值映射到属性上,从而会触发 set 的 notify 方法,则会更新 watcher,将值渲染到输入框里。

template

1
2
3
4
5
6
7
8
<div id="vue-app">
<div class="btnGroup">
<div>{{ mvvm }}</div>
<!-- <div>{{ a }}</div> -->
<div>{{ a.a }}</div>
<input v-model="mvvm" />
</div>
</div>

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function replace(fragment) {
Array.from(fragment.childNodes).forEach(node => {
let text = node.textContent;
let reg = /\{\{(.*)\}\}/;
if (node.nodeType === 3 && reg.test(text)) {
console.log(RegExp.$1); //mvvm,a ,a.a
let arr = RegExp.$1.split('.');
let val = vm;
arr.forEach(key => {
key = key.trim();
val = val[key];
console.log(val);
});
new Watcher(vm, RegExp.$1, function (newVal) {
node.textContent = text.replace(/\{\{(.*)\}\}/, newVal);
});
node.textContent = text.replace(/\{\{(.*)\}\}/, val);
}
<!-- 双向绑定 -->
// 判断元素是否为元素
if (node.nodeType === 1) {
// 获取当前dom节点上的属性
let nodeAttrs = node.attributes;
// console.log(nodeAttrs);
// 将类数组转化为数组进行循环遍历
Array.from(nodeAttrs).forEach(attr => {
let name = attr.name;
let exp = attr.value;
if (name.indexOf("v-") == 0) {
// 默认v-就为v-model
//把对应的值赋给输入框
node.value = vm[exp];
}
// 需要订阅一下,每次修改,就把对应的值赋给输入框
new Watcher(vm, exp, function (newVal) {
//当Watcher触发时,把最新的值赋给输入框
node.value = newVal;
});
//当输入框输入时,要有值映射到属性上
node.addEventListener('input', function (e) {
let newVal = e.target.value;
//值改变,则会触发set方法中的notify,则会更新watcher,将值渲染到输入框里
vm[exp] = newVal;
})
});
}
// 如果有子节点,需要再执行replace
if (node.hasChildNodes()) {
replace(node);
}
});
}

computed

      实现完了 Vue 的双向绑定,我们来看看怎么实现 computed 吧。computed 是依赖其它的属性计算所得出最后的值,computed 的值再 getter 执行后是会有缓存的,只有在它依赖的属性值改变之后,下一次获取 computed 的值时才会重新调用对应的 getter 来计算。如果一个数据依赖于其它数据,那么我们常用 computed 去解决,如果需要在某个数据变化时做一些事情,那么就更适合用 watch 了。接下来说说我实现 computed 的一个大致思路吧。首先,我们需要把当前 computed 对象上的属性一一挂载到当前实例上,然后主要使用Object.defineProperty的 getter 方法,去判断对应属性值上的是否是个函数,如果是函数,就把他放回去,因为默认就是 get 方法,如果不是的话,就去执行 get 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function Vue(options = {}) {
this.$options = options;
var data = (this._data = this.$options.data);
observer(data);
for (let key in data) {
Object.defineProperty(this, key, {
enumerable: true,
get() {
return this._data[key];
},
set(newVal) {
this._data[key] = newVal;
}
});
}
// 让这里的this都是当前的实例
initComputed.call(this);
new Compile(options.el, this);
}
//初始化computed
function initComputed() {
//保存this
let vm = this;
//获取computed属性
let computed = this.$options.computed;
//通过Object.keys拿到computed上的key以数组的形式输出
Object.keys(computed).forEach(key => {
Object.defineProperty(vm, key, {
// 判断computed后的是否为函数,如果为函数,就还回去,默认执行get方法,如果不是函数,就执行get方法
get:
typeof computed[key] === "function"
? computed[key]
: computed[key].get,
set() {}
});
});
}

有关 vue 实现原理的完整代码,可以上我的 Github中查看。

总结

Vue.js 是采用 Object.defineProperty 的 getter 和 setter,并结合观察者模式来实现数据绑定的。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。


vue实现原理

Observer 数据监听器,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者,内部采用 Object.defineProperty 的 getter 和 setter 来实现。

Compile 指令解析器,它的作用对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。

Watcher 订阅者, 作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数。

Dep 消息订阅器,内部维护了一个数组,用来收集订阅者(Watcher),数据变动触发 notify 函数,再调用订阅者的 update 方法。

      从图中可以看出,当执行 new Vue() 时,Vue 就进入了初始化阶段,一方面 Vue 会遍历 data 选项中的属性,并用 Object.defineProperty 将它们转为 getter/setter,实现数据变化监听功能;另一方面,Vue 的指令编译器 Compile 对元素节点的指令进行扫描和解析,初始化视图,并订阅 Watcher 来更新视图, 此时 Wather 会将自己添加到消息订阅器中(Dep),初始化完毕。当数据发生变化时,Observer 中的 setter 方法被触发,setter 会立即调用 Dep.notify(),Dep 开始遍历所有的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行相应的更新。

感谢以下文章,对我理解 vue 的基本原理有很大的帮助:

Copyright ©2019 guowj All Rights Reserved.

访客数 : | 访问量 :