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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<div id="app">
<h1>{{title}}</h1>
<input v-model="name" />
<h1>{{name}}</h1>
<button v-on:click="clickMe">点我</button>
</div>

<!-- 文本内容:<input id="#input" type="text"> -->
</body>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/complie.js"></script>
<script src="./js/index.js"></script>
<script>
const selfVue = new SelfVue({
el: '#app',
data: {
title: 'hello world',
name: ''
},
methods: {
clickMe() {
this.title = '哈哈哈'
console.log(123);
}
},
mounted() {
setTimeout(() => {
selfVue.title = "你好"
console.log(selfVue.name);
}, 2000)
}
})
</script>

</html>

SelfVue 对象

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
/**
*
* @param {Object} data 数据对象
* @param {Node} el dom节点
* @param {String} prop 字符串
*/
function SelfVue(options) {
this.data = options.data
this.methods = options.methods
Object.keys(this.data).forEach(key => this.proxyKeys(key))
observe(this.data)
new Compile(options.el, this)
options.mounted.call(this)
}

SelfVue.prototype = {
/**
*
* @param {String} key 属性名
*/
proxyKeys(key){ // 省略掉 .data 的操作
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get() {
return this.data[key]
},
set(newVal) {
this.data[key] = newVal
}
})
}
}

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
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
/**
*
* @param {Object} data 需要被观察的对象
*/
function Observer(data) { // 观察者
this.data = data
this.walk(data)
}

Observer.prototype = {
/**
* 为对象的所有属性进行劫持
* @param {Object} data 需要绑定的对象
*/
walk(data) {
Object.keys(data).forEach(key => this.definReactive(data, key, data[key]))
},
/**
* 进行数据属性劫持
* @param {Object} object 当前对象
* @param {String} key 属性名
* @param {*} val 属性值
*/
definReactive(object, key, val) {
const dep = new Dep()
observe(val) // 递归劫持
Object.defineProperty(object, key, { // 访问器属性
configurable: true,
enumerable: true,
get() {
if (Dep.target) { // 如果是Watcher第一次获取属性值,保存在 dep 中
dep.addSubs(Dep.target)
}
return val // 返回需要取的值
},
set(newValue) {
if (val == newValue) { // 值为改变,不触发更新视图
return
}
val = newValue
console.log('属性:' + key + '的值已被监听,当前值为:' + newValue);
dep.notify() // 跟新值后通知 watcher 更新试图
}
})
}
}

/**
* 判断是否为对象,是否应该劫持
* @param {Object} object 需要被监听的对象
* @returns
*/
function observe(object) {

if (!object || typeof object !== 'object') {
return
}
return new Observer(object)
}

/**
* 存储watcher的对象
*/
function Dep() {

}

Dep.prototype = {
subs: [],
/**
*
* @param {Watcher} sub 订阅者实例
*/
addSubs(sub) {
this.subs.push(sub)
},
/**
* 通知watcher更新视图
*/
notify() {
this.subs.forEach(sub => sub.update())
}
}
Dep.target = null // 静态属性

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
32
33
34

/**
* 订阅者对象
* @param {SelfVue} selfVue vue实例
* @param {String} prop 属性名
* @param {Function} callback 回调函数
*/
function Watcher(selfVue, prop, callback) {
this.callback = callback // 获取订阅后的回调函数
this.prop = prop // 属性
this.selfVue = selfVue // selfVue 实例
this.value = this.get() // 获取当前观察者的值
}

Watcher.prototype = {
update() { // 更新视图
this.run()
},
run() { // 更新视图的流程
let value = this.selfVue.data[this.prop] // 获取到 selfVue 的属性
let oldVal = this.value // 获取旧的值
if (value !== oldVal) { // 新旧值不相等
this.value = value // 更新旧值
this.callback.call(this.selfVue, value, oldVal) // 更新视图
}
},
// 获取当前值并缓存
get() {
Dep.target = this
let value = this.selfVue.data[this.prop]
Dep.target = null
return value
}
}

complie dom编译器对象

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/**
* 编译器对象
* @param {Node} el 元素节点
* @param {SelfVue} selfVue selfVue实例
*/
function Compile(el, selfVue) {
this.el = document.querySelector(el) // 获取当前元素
this.selfVue = selfVue // 获取 selfVue
this.fragment = null // 文档片段置空
this.init() // 初始化文档片段
}

Compile.prototype = {

init() {
if (this.el) {
this.fragment = this.nodeToFragment(this.el)
this.compileElement(this.fragment)
this.el.appendChild(this.fragment)
}else {
console.log('Dom元素不存在');
}
},
/**
* 元素节点变成文档片段
* @param {Node} el 元素节点
*/
nodeToFragment(el) {
const fragment = document.createDocumentFragment();
let child = el.firstChild
while (child) {
console.log(el, child);
fragment.appendChild(child) // 会将元素在原位置移除
child = el.firstChild
}
return fragment
},

/**
*
* @param {Node} el 元素节点
*/
compileElement(el) {
const childNodes = el.childNodes;
[...childNodes].slice().forEach((node) => {
const reg = /\{\{(.*)\}\}/;
const text = node.textContent
if (this.isTextNode(node) && reg.test(text)) { // 处理文本节点
this.compileText(node, reg.exec(text)[1]) // reg.exec(text)[1] -> 正则匹配{{}}的变量
}else if (this.isElementNode(node)) { // 处理元素节点
this.complie(node)
}
if (node.childNodes && node.childNodes.length) {
this.compileElement(node)
}
})
},
/**
*
* @param {Node} node 元素节点
*/
complie(node){
let nodeAttrs = node.attributes; // 获取当前元素上的属性
[...nodeAttrs].forEach(attr =>{
let attrName = attr.name // 获取属性名称
if (this.isDirective(attrName)) { // 判断是否有 v- 开头
let prop = attr.value // 获取属性值
let dir = attrName.substring(2) // 得到去掉 v- 之后的字符串
if (this.isEventDirective(dir)) { // 判断是否为绑定事件
this.complieEvent(node, this.selfVue, prop, dir) // 添加事件
}else {
this.complieModel(node, this.selfVue, prop, dir) // 数据的双向绑定
}
node.removeAttribute(attrName) // 移除属性名
}
})
},

/**
*
* @param {Node} node 元素节点
* @param {String} prop selfVue 的属性名
*/
compileText(node, prop) { // 处理文本节点
let initText = this.selfVue[prop] // 获取 selfVue 中的属性名
this.updateText(node, initText) // 处理文本节点的值
new Watcher(this.selfVue, prop, (value) => this.updateText(node, value)) // 为该属性设置观察者
},
/**
* 处理事件绑定
* @param {Node} node 元素节点
* @param {SelfVue} selfVue selfVue实例
* @param {String} prop 属性名
* @param {*} dir 去掉 v- 之后的事件
*/
complieEvent(node, selfVue, prop, dir) {
let eventType = dir.split(':')[1] // 获取事件类型
let cb = selfVue?.methods?.[prop] //获取事件回调函数
if (eventType && cb) { // 判断事件名称和回调函数是否都不为空
node.addEventListener(eventType, cb.bind(selfVue), false) // 问当前元素绑定事件
}else{
throw new Error(`事件类型不可以为${eventType}${prop}不可以为${cb}`)
}
},
/**
*
* @param {Node} node 元素节点
* @param {SelfVue} selfVue vue实例
* @param {String} exp 属性名
* @param {*} dir
*/
complieModel(node, selfVue, prop, dir) {
let val = selfVue[prop] // 获取当前属性
this.modelUpdater(node, val) // 处理节点内部的值
new Watcher(selfVue, prop, value => this.modelUpdater(node, value)) // 为当前属性绑定观察者
node.addEventListener('input', (e) => { // 这里只处理了输入框的事件
let newValue = e.target.value // 得到输入的值
if(val == newValue)
return
selfVue[prop] = newValue // 进行渲染
val = newValue // 不可去掉,会出现BUG
})
},
/**
*
* @param {Node} node 元素节点
* @param {*} value 数据
*/
updateText(node, value) { // 处理未定义的文本节点为空字符串
node.textContent = typeof value == "undefined" ? '' : value
},
/**
*
* @param {Node} node 元素节点
* @param {*} value
* @param {*} oldValue
*/
modelUpdater(node, value, oldValue) { // 处理未定义的节点的内容的值为空字符串
node.value = typeof value == "undefined" ? '' : value
},
/**
* 判断是否有 v- 开头
* @param {String} attr 属性名称
* @returns 是否有 v- 开头
*/
isDirective(attr) {
return attr.indexOf('v-') == 0
},
/**
* 判断是否 on: 开头
* @param {String} dir 去掉 v- 后的字符串
* @returns 是否 on: 开头
*/
isEventDirective(dir) {
return dir.indexOf('on:') == 0
},
/**
*
* @param {Node} node 元素节点
*/
isElementNode(node) { // 判断是否为元素节点
return node.nodeType == 1
},
/**
*
* @param {Node} node 元素节点
* @returns
*/
isTextNode(node) { // 判断是否为文本节点
return node.nodeType == 3
}

}

TODO 由于时间关系,日后再详细分解讨论双向绑定的实现,先记录代码,有详细的注释