Vue的简单实现和解读

一、Compile类的实现

​ 首先vue的一些属性和代码并不是JavaScript原生具有的,所以我们需要对相关的内容进行编译,对虚拟节点进行编译。首先新建一个index.html的文件作为我们的入口里面有这些元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body>
<div id="app">
<input type="text" v-model="message.a"/>
<div>{{message.a}}</div>
<div>{{test}}</div>
<ul><li>{{message.a}}</li></ul>
</div>
</body>
<script>
let vm = new MVVM({
el:'#app',
data: {
test: 'I love Vue',
message:{
a:'This is my first Vue test'
}
}
})
</script>

​ 然后新建一个MVVM.js文件,新建一个MMVM类,在构造函数中获取挂载的节点和依赖的相关数据。

1
2
3
4
5
6
7
8
constructor(options) {
this.$el = options.el;
this.data = options.data;
// 如果有可编译的模板
if(this.$el){
new Compile(this.$el, this);
}
}

​ 只要获取到了需要去挂载的组件和依赖的数据,我们就能够去对相关的内容进行编译,于是继续新建一个Compile文件进行编译的相关操作。

​ 新建一个编译类Compile,这里新建类的原因是我们之后会在这个类中定义一些工具函数,放在一个类中相对便于管理。然后编写Compile类的构造函数。这里我们给Compile类的构造函数传入的参数是需要挂载的对象el和虚拟节点vm。首先我们需要判断挂载的el是否是一个真实的元素节点,如果是一个元素节点我们就直接给Compile这个类的el赋值为el,如果不是就利用querySelector这个函数去寻找这个节点。这里我们在判断el是否为元素节点的时候会去判断nodeType。一般常用的nodeType有三种:1代表该节点是一个元素节点,2代表该节点是一个属性节点,3代表该节点是一个文本节点。如果挂载的节点找到了并且确实存在,那这个时候我们就可以正式开始进行编译操作了。考虑到编译的时候由于数据的渲染等因素影响会造成页面进行大量的回流操作,从而影响JavaScript运行效率,我们通常会将页面中真实的DOM移入到内存中,并对其进行相关的操作。这样就不会引起大量的回流。所以我么利用createDocumentFragment在内存中创建一个DocumentFragment,然后对原本在节点中的元素进行复制。

1
2
3
4
5
6
7
8
node2fragment(el) {
let fragment = document.createDocumentFragment();
let childNode;
while(childNode = el.firstChild){
fragment.appendChild(childNode);
}
return fragment;
}

​ 现在我们对fragment里面的元素进行编译,遍历fragment数组里面的所有元素,如果是node节点的元素,我们就以node节点的编译方式进行编译,如果是一个文本节点,就以文本节点的方式进行编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
compile(fragment){
let chiledNodes = fragment.chiledNodes;
Array.from(childNodes).forEach((node)=>{
if(this.isElementNode(node)){
// 如果是元素节点,我们还需要深入这个节点中进行编译
this.compileElement(node);
// 这里需要编译元素
this.compile(node);
} else {
this.compileText(node)
}
})
}

​ 首先我们进行的是对元素节点的编译。在compileElement里面先获取到这个节点中的所有属性。遍历这些属性找到v-开头的属性,这些直接和vue指令相关。将这些属性之后的值和命令取出,之后会对应一个操作,该操作我们之后会提到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
compileElement(node) {
// 获取这个节点的所有属性
let attrs = node.attributes;
Array.from(attrs).forEach((attr)=>{
let attrName = attr.name;
// 如果属性中含有v-的属性
if(this.isDirective(attrName)){
// expr获得绑定的值
let expr = attr.value;
let [,type] = attr.spilit('-');
// 执行一些操作。。。
}
})
}

​ 之后我们继续编译文本节点,进行编译的时候首先我们会将这个文本节点的内容取出,然后进行正则匹配查看是否满足渲染的规范,之后进行一些操作。

1
2
3
4
5
6
7
compileText(node) {
let expr = node.nodeContent;
let reg = /\{\{([^]+)\}\}/g;
if(reg.test(expr)){
// 进行与文本节点有关的操作
}
}

二、响应式的实现

​ 然后我们需要对数据的更新进行相关的处理。为了方便统一管理这些操作我们建立一个CompileUtils类,这个类中手下我们定义一个方法model用来处理input中的数据的变化。这里就需要用到我们的响应式原理了。

​ 我们继续处理input这个元素节点。首先我们定义的是model更新的变化处理函数。然后建立一个watcher类用于建立一个观察者模式,也是作为和之后的observer的中介。然后我们给input这样的节点添加一个input事件,当用户有输入的行为的时候,我们就会及时的获取input中的值并将绑定的数据改变。之后为了在编译阶段输入框就能够相应到data中的数据,我们就调用了updateFn函数进行数据的渲染。

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
// 定义编译的工具类
CompileUtils = {
getVal(vm, expr){
expr = expr.split('.');
return expr.reduce((prev,next)=>{
return prev[next];
},vm.$data);
},
setVal(vm,expr, value) {
expr = expr.split('.');
// 利用reduce解剖多重对象
return expr.reduce((prev,next,currentIndex)=>{
if(currentIndex == expr.length-1)
return prev[next] = value;
return prev[next];
},vm.$data);
},
// 检查输入框的变化
model(node,vm,expr) {
// 获得相关的更新处理函数
let updateFn = this.updater['modelUpdate'];
// 创建一个watch类观察变化
new Watcher(vm,expr,()=>{
updateFn && updateFn(node, this.getVal(vm,expr));
})
// 给节点创建一个input事件监听器
node.addEventListener('input',()=>{
let newValue = e.target.value;
this.setVal(vm,expr,newValue);
})
// 触发updateFn()方法
updateFn && updateFn(node,this.getVal(vm,expr));
},
updater: {
// 文本更新
textUpdater(node, value){
node.textContent = value;
},
// 输入框更新
modelUpdater(node, value){
node.value = value;
}
}
}

​ 然后我们将焦点聚在watcher这个类上。通过观察上面的代码可知,在构造watcher类的时候我们会给他传入vm,expr和回调函数cb。其中,回调函数cb是用在observer用来发布订阅的时候通知相关的组件及进行数据的重新渲染。我们构建的watcher函数如下所示:

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
// 观察着的目的就是给需要变化的那个元素增加一个观察者,数据变化后执行那个方法
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先获取一下老的值
this.value = this.get();
}
getVal(vm, expr) {
expr = expr.split('.');
return expr.reduce((prev, next)=>{
return prev[next];
}, vm.$data);
}
get() {
Dep.target = this;
let value = this.getVal(this.vm, this.expr);
Dep.target = null;
return value;
}
// 对外暴露的方法
update() {
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value;
if(newValue != oldValue){
this.cb(newValue)
}
}
}
// 用新值和老值进行对比

​ 在构造函数里面会调用watcher的get方法,从而能够构建observer对这个watcher的控制,在get方法中首先我峨嵋你将Dep的target属性指向了当前这个watcher对象,然后我们会调用getVal逐层找到依赖的数据。当我们在获取这个value的值的时候,实际上我们会触发这个值的get()方法。于是我们就可以在get方法里面提娜佳对这个值的订阅,当这个值改变的时候我们就会触发notify方法通知每一个订阅者重新获取这个值的数据。因此,我们的observer类是这样的:

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
class Observer{
constructor(data) {
this.observe(data);
}
observe(data){
// 要对这个data数据 将原有的属性
if(!data || typeof data != 'object'){
return;
}
Object.keys(data).forEach(key=>{
this.defineReactive(data,key,data[key]);
this.observe(data[key]);
})
}
defineReactive(obj, key, value){
var that = this;
// 对每一个data都会对应一个
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue) { // 当给data中的属性设置值的适合 更改获取的属性的值
if(newValue != value){
// 这里的this不是实例
that.observe(newValue);
value = newValue;
dep.notify(); // 通知所有人数据更新了
}
}
})
}
}

class Dep {
constructor() {
this.subs = [];
}
addSub(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach(watcher=>watcher.update())
}
}

​ 在Observer类中,我们对逐个的对数组中的每个对象执行监听,如果发现我们当前正在读取这个值,就会触发get方法添加对这个值的依赖,如果在修改这个值,就会调用notify方法,去调用所有依赖这个值的节点去做响应的处理。Watcher类中的update方法就是这里的回调函数。

​ 这样的话,我们就完成了整个响应式的完成,我们可以去尝试打开test.html进行相关的操作,和我们真正使用Vue框架的效果一致。

三、Vue2.0和Vue3.0

​ Vue2.0就是采用我们刚刚使用的Object.defineProperty这个函数,通过调用get和set方法从而实现了整个响应式的流程。但是如果我们关注Vue3.0的最新态势的话,会发现Vue3.0在做响应式的时候已经采用了ES6的新属性——Proxy。Proxy会拦截JavaScript引擎内部对于相应行为的底层操作并执行自定义的相应行为的陷阱函数。

​ 为什么使用Proxy来代替Object.defineProperty实现响应式?

​ 首先先来说目前的Vue2.0使用Object.defineProperty的缺陷。首先是Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。虽然在Vue的源码中,人们尝试利用switch检查类似pop、push的方法来实现响应式,但是如果我们直接给数组的某一项赋值的时候是无法检测到变化的。

​ 其次是Object.defineProperty只能够劫持对象的属性,因此定义响应式的时候需要通过每个对象的属性进行递归和遍历来实现。因此需要在这一点上进行i优化。

​ proxy针对以上问题给出了解决的办法。proxy可以劫持整个对象,并且返回一个新的对象。同时他也定义了13中劫持的操作,能够提供我们大部分的应用场景,提高我们的开发效率。

​ 由于现在Vue3.0暂时还没有正式发布,但是我么也可以利用proxy来写一个简单点的MVVM框架。

0%