easycodesniper

blog by chen qiyi

Vue2指令与生命周期

Vue类的创建

在开始我们要创建一个Vue类来模拟Vue整体的框架

1
2
3
export default class Vue {
constructor() {}
}

在入口文件中将Vue类导入,并设置为全局属性

1
2
3
import Vue from "./Vue";

window.Vue = Vue

在html中实例化一个Vue类,并模拟Vue的挂载方式

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div id="app">

</div>
<script>
new Vue({
el: '#app',
data: {
a: 10
}
})
</script>
</body>

当Vue类被实例化之后,Vue内部就会进行数据响应式处理和模板编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Compile from "./Compile"

export default class Vue {
constructor(options) {
//保存整个挂载对象,以便后面的使用
this.$options = options || {}
//保存传入的data数据
this._data = options.data || undefined

//将数据变成响应式

//模板编译
new Compile(options.el, this)
}
}

数据响应式部分

在这里我们需要导入之前已经完成的数据响应式模块的代码,
通过将传入的数据作为参数赋值给observe函数,来实现数据的响应式
然后调用_initData()函数将响应式的数据挂载到实例的身上

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
import Compile from "./Compile"
import { observe } from "./数据响应式/observe"

export default class Vue {
constructor(options) {
this.$options = options || {}
this._data = options.data || undefined

//将数据变成响应式
observe(this._data)
//将传入的options挂载到实例身上
this._initData()
//模板编译
new Compile(options.el, this)
}

_initData() {
var self = this
Object.keys(this._data).forEach(key => {
Object.defineProperty(self, key, {
get() {
return self._data[key]
},
set(newVal) {
self._data[key] = newVal
}
})
})
}
}

模板编译部分

我们来研究模板编译部分

我们主要通过一个Compile类来进行模板编译,它要求传入挂载的容器以及整个Vue实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default class Compile {
constructor(el, vue) {
//保存整个vue实例,以便后续使用
this.$vue = vue
//保存挂载的容器
this.$el = document.querySelector(el)
//如果用户传入了挂载点
if(this.$el) {

//实际上用的是AST,但我们现在主要是学习指令和生命周期,所以用一个简单的函数代替
//相当于一个轻量级的AST
this.node2Fragment(this.$el)
}
}

node2Fragment(el) {}

}

node2Fragment函数

我们使用node2Fragment函数来代替AST,作为一个AST的轻量级。

在这里我们会用到一个JS方法document.createDocumentFragment()

createDocumentFragment()方法创建了一虚拟的节点对象,节点对象包含所有属性和方法。
当你想提取文档的一部分,改变,增加,或删除某些内容及插入到文档末尾可以使用createDocumentFragment() 方法。

我们在这里需要用到的就是:当向createDocumentFragment创建的虚拟节点对象插入dom树中的某一节点,那么这个节点就会从原来的dom树中删除

1
2
3
4
5
6
7
8
9
10
11
node2Fragment(el) {
var child
//创建一个虚拟节点对象
var fragment = document.createDocumentFragment()
//将挂载容器中的第一个孩子赋值给child,因为是引用类型值,将child添加到fragment对象中会使el的原dom树中的第一个孩子节点下树,
//循环一直到el中的所有子节点都被添加fragment中
while(child = el.firstChild) {
fragment.appendChild(child)
}
return fragment
}

通过node2Fragment函数得到fragment虚拟节点对象,然后传给compile函数进行编译

compile函数

在compile函数中会对传入的挂载容器的虚拟节点对象中的节点进行判断
如果是元素节点,就交给compileElement函数处理。如果是文本节点(元素节点之间的换行符也包括在内),就交给compileText函数处理。

当完成compile函数进行的模板编译之后,要将解析好的虚拟节点再重新上树

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
export default class Compile {
constructor(el, vue) {
//保存整个vue实例,以便后续使用
this.$vue = vue
//保存挂载的容器
this.$el = document.querySelector(el)
//如果用户传入了挂载点
if(this.$el) {

//实际上用的是AST,但我们现在主要是学习指令和生命周期,所以用一个简单的函数代替
//相当于一个轻量级的AST
this.$fragment = this.node2Fragment(this.$el)
//编译fragment
this.compile(this.$fragment)

//将解析好的虚拟节点上树
this.$el.appendChild(this.$fragment)
}
}

node2Fragment(el) {
// ....
}

compile(fragment) {
//获取挂载容器(生成的虚拟节点对象)的所有子节点
const childNodes = fragment.childNodes
//保存一份this,因为在下面用到的箭头函数中this不一定指向这个函数的上下文
var self = this

childNodes.forEach(node => {
//console.log('node:',node)

if(node.nodeType == 1) { //如果这个子节点是dom节点
//调用compileElement函数对这个节点进行处理
self.compileElement(node)
} else if(node.nodeType == 3){ //如果是文本节点以及一些换行符等
//调用compileText函数对文本节点进行处理
let text = node.textContent
self.compileText(text)
}
})
}

compileElement(node) {}

compileText(text) {}
}

compileElement函数

compileElement函数

compileElement函数内部会去看看有没有vue中的一些指令(例如:v-if,v-model等),如果有的话,就解析指令并进行相应操作

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
compileElement(node) {
//使用attributes可以得到dom节点上的属性,
//它的方便之处在于得到的不是字符串类型的属性,而是属性列表
let nodeAttrs = node.attributes

//类数组对象转化成数组
Array.prototype.slice.call(nodeAttrs).forEach(attr => {
//在这里对v-if等指令进行分析
//console.log('attr:',attr);
//得到属性名和属性值
let attrName = attr.name
let attrValue = attr.value

if(attrName.indexOf('v-') == 0) {
//如果是v-if等指令,那么我们需要得到'v-'之后的东西,来确定具体的指令操作
let dir = attrName.substring(2)
//console.log('dir:',dir);
if(dir == 'if') { //发现是v-if指令
// ... 对指令的具体操作
} else if(dir == 'model') { //发现是v-model指令
// ... 对指令的具体操作
}
}
})
}

在html中我们用如下代码进行测试:

1
2
3
4
5
6
7
<div id="app">
<h3 class="aaa" v-if="if" >{{a}}</h3>
<ul>
<li>A</li>
<li>B</li>
</ul>
</div>

attr中的内容:

dir的内容:

分析指令操作

我们以v-model为例来分析Vue中指令的操作
使用如下html来分析数据双向绑定v-model

1
2
3
4
<div id="app">
{{b.m.n}}
<input type="text" v-model="b.m.n">
</div>

先解决双向绑定的第一层,即input输入框能显示得到数据
具体思路就是我们先获取到v-model绑定的属性的值,然后将他赋值给input输入框

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
compileElement(node) {
//使用attributes可以得到dom节点上的属性,
//它的方便之处在于得到的不是字符串类型的属性,而是属性列表
let nodeAttrs = node.attributes
var self = this
//类数组对象转化成数组
Array.prototype.slice.call(nodeAttrs).forEach(attr => {
//在这里对v-if等指令进行分析
console.log('attr:',attr);
//得到属性名和属性值
let attrName = attr.name
let attrValue = attr.value

if(attrName.indexOf('v-') == 0) {
//如果是v-if等指令,那么我们需要得到'v-'之后的东西,来确定体的指令操作
let dir = attrName.substring(2)
if(dir == 'model') {
//通过getVueVal函数得到v-model绑定的数据的值
let v = self.getVueVal(self.$vue, attrValue)
//将input输入框的值设置为v-model绑定的数据的值
node.value = v
}
}
})
}

getVueVal(vue, exp) {
var val = vue
//这个属性可能需要连续点语法处理,例如a.m.n
//我们就需要将他拆分开再使用中括号语法组合起来,因为js无法识别obj[a.m.n]的形式
exp = exp.split('.')
exp.forEach(k => {
val = val[k]
})
return val
}

这就实现了在输入框中显示v-model绑定的数据

双向绑定的第二层,input输入框中改变数据,页面渲染的数据也相应改变

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
compileElement(node) {
//使用attributes可以得到dom节点上的属性,
//它的方便之处在于得到的不是字符串类型的属性,而是属性列表
let nodeAttrs = node.attributes
var self = this
//类数组对象转化成数组
Array.prototype.slice.call(nodeAttrs).forEach(attr => {
//在这里对v-if等指令进行分析
//得到属性名和属性值
let attrName = attr.name
let attrValue = attr.value

if(attrName.indexOf('v-') == 0) {
//如果是v-if等指令,那么我们需要得到'v-'之后的东西,来确定具体的指令操作
let dir = attrName.substring(2)
if(dir == 'model') {
let v = self.getVueVal(self.$vue, attrValue)
node.value = v
//给属性添加Watcher监听,当它的数据发生改变就会重新渲染
new Watcher(self.$vue, attrValue, val => {
node.value = val
})
//给input框绑定输入事件
node.addEventListener('input', e => {
//获取到input框中的值
var newVal = e.target.value
//将属性的值设置为取得的值
self.setVueVal(self.$vue, attrValue, newVal)
})

}
}
})
}

compileText函数

如果是一个文本节点,那么就需要看看文本中有没有双大括号语法,如果有的话,要将双大括号内的东西解析成具体数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
compileText(node, name) {
//在Vue类的实例中寻找双大括号中的元素,并将他解析好
node.textContent = this.getVueVal(this.$vue, name)
}

getVueVal(vue, exp) {
var val = vue
//这个属性可能需要连续点语法处理,例如a.m.n
//我们就需要将他拆分开再使用中括号语法组合起来,因为js无法识别obj[a.m.n]的形式
exp = exp.split('.')
exp.forEach(k => {
val = val[k]
})
return val
}

我们使用下面的例子来检测函数功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
{{b.m.n}}
<ul>
<li>A</li>
<li>B</li>
</ul>
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
a: 10,
b: {
m: {
n: 100
}
}
}
})
</script>

我们来看看传入compileText函数的双大括号内的属性(name),以及解析好之后的具体属性值

上树之后的页面:

虽然完成了双大括号的编译,也渲染到了页面上。但是当数据发生了改变时页面不会跟着改变。
所以我们需要给这个数据加上一个Watcher来监听它的变化,然后通知组件更新

1
2
3
4
5
6
7
8
9
compileText(node, name) {
//console.log('name:',name);
//在Vue类的实例中寻找双大括号中的元素,并将他解析好
node.textContent = this.getVueVal(this.$vue, name)
//console.log('node.textContent:',node.textContent);
new Watcher(this.$vue, name, val => {
node.textContent = val
})
}

得到的结果:

最后

实例化Vue类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<body>
<div id="app">
{{b.m.n}}
<input type="text" v-model="b.m.n">
</div>
<script src="/xuni/bundle.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
a: 10,
b: {
m: {
n: 100
}
}
}
})
</script>
</body>

Vue类

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
import Compile from "./Compile"
import { observe } from "./数据响应式/observe"
import Watcher from "./数据响应式/Watcher"

export default class Vue {
constructor(options) {
this.$options = options || {}
this._data = options.data || undefined

//将数据变成响应式
observe(this._data)
//将传入的options挂载到实例身上
this._initData()
//模板编译
new Compile(options.el, this)
}

_initData() {
var self = this
Object.keys(this._data).forEach(key => {
Object.defineProperty(self, key, {
get() {
return self._data[key]
},

set(newVal) {
self._data[key] = newVal
}
})
})
}
}

Compile类

Compile类主要用来处理模板编译的问题

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import Watcher from "./数据响应式/Watcher"

export default class Compile {
constructor(el, vue) {
this.$vue = vue
this.$el = document.querySelector(el)
//如果用户传入了挂载点
if(this.$el) {
//实际上用的是AST,但我们现在主要是学习指令和生命周期,所以用一个简单的函数代替
//相当于一个轻量级的AST
this.$fragment = this.node2Fragment(this.$el)
//编译得到的fragment
this.compile(this.$fragment)
//将解析好的虚拟节点上树
this.$el.appendChild(this.$fragment)
}
}
node2Fragment(el) {
var child
var fragment = document.createDocumentFragment()

while(child = el.firstChild) {
fragment.appendChild(child)
}

return fragment
}

compile(fragment) {
console.log(fragment);
//获取挂载容器的所有子节点
const childNodes = fragment.childNodes
//保存一份this,因为在下面用到的箭头函数中this不一定指向这个函数的上下文
var self = this
//捕获双大括号中内容
var reg = /\{\{(.+)\}\}/
childNodes.forEach(node => {
console.log('node:',node)

if(node.nodeType == 1) {
self.compileElement(node)
} else if(node.nodeType == 3){
let text = node.textContent
if(reg.test(text)) {
console.log('匹配成功');
//双括号中的内容,即双括号语法中的数据
let name = text.match(reg)[1]
self.compileText(node, name)

}
}
})
}

compileElement(node) {
//使用attributes可以得到dom节点上的属性,
//它的方便之处在于得到的不是字符串类型的属性,而是属性列表
let nodeAttrs = node.attributes
var self = this

//类数组对象转化成数组
Array.prototype.slice.call(nodeAttrs).forEach(attr => {
//在这里对v-if等指令进行分析
//得到属性名和属性值
let attrName = attr.name
let attrValue = attr.value

if(attrName.indexOf('v-') == 0) {
//如果是v-if等指令,那么我们需要得到'v-'之后的东西,来确定具体的指令操作
let dir = attrName.substring(2)
if(dir == 'model') {
let v = self.getVueVal(self.$vue, attrValue)
node.value = v

new Watcher(self.$vue, attrValue, val => {
node.value = val
})
node.addEventListener('input', e => {
var newVal = e.target.value
self.setVueVal(self.$vue, attrValue, newVal)
})

}
}
})
}

compileText(node, name) {
//在Vue类的实例中寻找双大括号中的元素,并将他解析好
node.textContent = this.getVueVal(this.$vue, name)
new Watcher(this.$vue, name, val => {
node.textContent = val
})
}

getVueVal(vue, exp) {
var val = vue
//这个属性可能需要连续点语法处理,例如a.m.n
//我们就需要将他拆分开再使用中括号语法组合起来,因为js无法识别obj[a.m.n]的形式
exp = exp.split('.')
exp.forEach(k => {
val = val[k]
})

return val
}

setVueVal(vue, exp, value) {
var val = vue
exp = exp.split('.')

exp.forEach((k, i) => {
//在一层层找属性时,如果到了最后一层,就将它设置为新的值
//例如:b.m.n 我们要改变的是b.m.n中最后一层n的值,前面几层只需要迭代下来就可以了
if(i != exp.length - 1) {
val = val[k]
} else {
val[k] = value
}
})
}

}