Vue双向数据绑定的原理与实现

一、关于Object.defineProperty


Object.defineProperty[1]是实现Vue双向数据绑定的核心,它的语法如下:

1
Object.defineProperty(obj, prop, descriptor)

参数:

  • obj:要定义属性的对象
  • prop:要定义或修改的属性的名称
  • descriptor:要修改的值
    返回值:
  • 被传递给函数函数的对象

实例:

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
let obj = {
id: 1,
pname: '小米',
price: 1222
}

// 修改price属性
Object.defineProperty(obj, 'price', {
value: 1999
})
console.log(obj)

// 增加count属性
Object.defineProperty(obj, 'count', {
value: 100
})
console.log(obj)

// id属性不可写入
Object.defineProperty(obj, 'id', {
writable: false
})
obj.id = 3
console.log(obj)

Object.defineProperty(obj, 'address', {
value: '山东',
enumerable: false, // 设置address属性不可被遍历到
configurable: false // 设置address属性不可被修改或删除
})
console.log(obj)
console.log(Object.keys(obj)) // 没有address属性
delete obj.address
console.log(obj)

运行结果:


二、双向数据绑定的极简模拟


Vue框架最核心的功能就是双向数据绑定,所以实现了双向数据绑定,其实就相当于做了一个最简单的Vue框架。
而Vue双向数据绑定的核心实现思路,就是利用Object.defineProperty给每一个变量都添加get和set属性,让变量被修改时,将数据同步到对应HTML中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let vm = {}
Object.defineProperty(vm, 'txt', {
get: function() {},
set: function(val) {
// 当属性被赋值时,就通过DOM操作将新数据更新到对应的节点内容上
var span = document.getElementsByTagName('span')[0]
span.innerHTML = val
}
})

// 给input添加事件,输入的时候可以修改到span中的值
var input = document.getElementsByTagName('input')[0]
input.oninput = function() {
model.txt = input.value
}

三、双向数据绑定的实现原理


Vue的主要架构由以下几部分组成:

flowchart LR
    vue["new Vue()"] --> observe["Observer\n监听所有变量"] & compile["Compile\n解析指令"]
    observe -->|"通知变化"| dep([Dep]) -->|"通知变化"| watcher[Watcher]
    compile -->|"订阅数据变化\n绑定更新函数"| watcher
    compile -->|"初始化视图"| updater([Updater])
    watcher -->|"添加订阅者"| dep 
    watcher -->|"更新视图"| updater

其中:

  • 数据监听器Observer
    Observer会利用Object.defineProperty给所有属性都加上getter和setter函数,这样当数据被赋值时,就会触发setter函数,将变化通知给订阅者。从而实现了监听数据的功能
  • 指令解析器Compile
    根据传入的根节点,对每个HTML节点进行扫描和解析。根据模板规则将模板变量(即被双大括号囊括的变量)替换成对应的值,并处理特定的属性,如给v-model添加订阅者,给v-on绑定对应事件监听等。
  • 订阅者Watcher
    Watcher是Observer和Compile之间的通信桥梁,它会订阅Observer中属性值变化的消息,当属性值变化时,Wathcer就会触发Compile中对应的更新函数,将新数据刷新到视图中
  • 订阅器Dep
    订阅器采用了“订阅-发布”设计模式,主要用来收集订阅者Watcher,对Observer和Watcher进行统一管理

四、双向数据绑定代码实现


1. 实现SelfVue主逻辑代码

index.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
function SelfVue(options) {
var self = this
// 变量
this.data = options.data
// 方法
this.methods = options.methods

// 将变量都挂到SelfVue上面
Object.keys(this.data).forEach(key => self.proxyKey(key))

// 每个变量挂监听器
observe(this.data)

// 解析节点
new Compile(options.el, this)

// 计算属性没有实现,只是简单的调用函数
options.mounted.call(this)
}

SelfVue.prototype = {
proxyKey: function (key) {
let self = this
Object.defineProperty(self, key, {
enumerable: false,
configurable: true,
get: function setter() {
return self.data[key] // 其实就是修改SelfVue里面data的属性
},
set: function getter(newVal) {
self.data[key] = newVal
}
})
}
}

2. 实现监听器Observer

observer.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
53
54
55
56
57
58
59
60
function Observer(data) {
this.data = data
this.walk(data)
}

Observer.prototype = {
walk: function (data) {
var self = this
// 给每个变量都添加监听器
Object.keys(data).forEach(key => self.defineReactive(data, key, data[key]))
},
defineReactive: function (data, key, val) {
// 实例化订阅器
var dep = new Dep()
// 递归遍历所有子属性
observe(val)
// 给每个变量挂上set和get函数
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function setter() {
// 在赋值的时候给订阅器添加更新视图的函数
if (Dep.target) Dep.addSub(Dep.target)
return val
},
set: function getter(newVal) {
// 数据不变时不做任何操作
if (val === newVal) return
// val在闭包的环境下被永久保存了
val = newVal
dep.notify()
}
})
}
}

observe(value) {
// 如果属性的值为空,或者属性不是对象,结束递归
if (!value || typeof value !== 'object') return
// 给属性创建新的监听器
return new Observer(value)
}

function Dep() {
// 订阅器集
this.subs = []
}

Dep.prototype = {
// 添加订阅者
addSub: function (sub) {
this.subs.push(sub)
},
// 通知变化,更新视图
notify: function () {
this.subs.forEach(sub => sub.update())
}
}
// 更新视图的函数,静态属性
Dep.target = null

3. 实现指令解析器Compile

compile.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
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
function Compile(el, vm) {
// 即view-model,Vue本身就是一个view-model
this.vm = vm
// 获取根节点
this.el = document.querySelector(el)
this.init()
}

Compile.prototype = {
init: function () {
if (this.el) {
// 将el节点转换成fragment
this.fragment = this.nodeToFragment(this.el)
// 处理节点中的Vue语法
this.compileElement(this.fragment)
// 将fragment放回到el中
this.el.appendChild(this.fragment)
return
}
console.log('DOM不存在')
},
nodeToFragment: function(el) {
// Fragment适用于大量DOM操作
var fragment = document.createDocumentFragment()
var child = el.firstChild
// 当节点中有还有子节点时继续遍历
while(child) {
// 将子节点移入fragment
fragment.appendChild(child)
child = el.firstChild
}
return fragment
},
compileElemnt: function (el) {
// 获得所有子节点
var childNodes = el.childNodes
var self = this

// Array.prototype.slice用于将类数组转换成数组
Array.prototype.slice.call(childNodes).forEach(node => {
// 匹配模板变量
var reg = /\{\{\s*(.*?)\s*\}\}/
var text = node.textContent

// 判断是否是普通元素节点
if (self.isElementNode(node)) {
self.compile(node)
}
// 判断是否是文本节点,并匹配模板变量
else if (self.isTextNode(node) && reg.test(text)) {
// 将模板变量替换成对应的值
self.compileText(node, reg.exec(text)[1])
}

// 递归遍历子节点
if (node.childNodes && node.childNodes.length) {
self.compileElement(node)
}
})
},
compile: function (node) {
var nodeAttrs = node.attributes
var self = this

Array.prototype.forEach.call(nodeAttrs, attr => {
var attrName = attr.name
// 判断是否是v-开头的属性
if (self.isDirective(attrName)) {
var exp = attr.value
// 去掉v-
var dir = attrName.substring(2)

// 判断是否是事件
if (self.isEventDirective(dir)) {
self.compileEvent(node, self.vm, exp, dir)
}
// 如果不是那就是双向绑定
else {
self.compileModel(node, self.vm, exp, dir)
}

// 删除原属性
node.removeAttribute(attrName)
}
})
},
compileEvent: function (node, vm, exp, dir) {
// 获得事件类型
var eventType = dir.split(':')[1]
// 获得对应方法
var cb = vm.methods && vm.methods[exp]

// 监听事件
if (eventType && cb) node.addEventListener(eventType, cb)
},
compileModel: function (node, vm, exp, dir) {
var self = this
// 获取属性值
var val = vm[exp]
// 更新视图
this.modelUpdater(node, val)
// 实例化订阅者,绑定更新视图函数
new Watcher(this.vm, exp, value => self.modelUpdater(node, value))
// 绑定输入框事件,输入时会同步更新视图
node.addEventListener('input', e => {
var newVal = e.target.value
if (val === newVal) return
self.vm[exp] = newVal
val = newVal
})
},
compileText: function (node, exp) {
var self = this
var text = self.vm[exp]
this.updateText(node, text)
new Watcher(this.vm, exp, value => self.updateText(node, value))
},
isElementNode: function (node) {
return node.nodeType === 1
},
isTextNode: function (node) {
return node.nodeType === 3
},
isDirective: function (attr) {
return attr.indexOf('v-') === 0
},
isEventDirective: function (attr) {
return attr.indexOf('on:') === 0
},
modelUpdater: function (node, value) {
node.value = typeof value == 'undefined' ? '' : value
},
updateText: function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value
}
}

4. 实现订阅器Watcher

watcher.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
function Watcher(vm, exp, cb) {
this.vm = vm
this.cb = cb
this.exp = exp
this.value = this.get()
}

Watcher.prototype = {
update: function () {
this.run()
},
run: function() {
var newVal = this.vm[this.exp]
var oldVal = this.value

if (newVal !== oldVal) {
this.value = newVal
// 数据变化时执行更新视图函数
this.cb.call(this, newVal)
}
},
get: function() {
Dep.target = this
// get属性的时候,会将订阅者push进对应属性的dep订阅器中
var value = this.vm[this.exp]
Dep.target = null
return value
}
}

5. 写一个测试页面

index.html:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>SelfVue</title>
</head>
<style>
#app {
text-align: center;
}
</style>
<body>
<div id="app">
<h2>{{title}}</h2>
<input v-model="name" />
<h1>{{name}}</h1>
<button v-on:click="clickMe">click me!</button>
</div>
</body>
<script src="js/observer.js" />
<script src="js/watcher.js" />
<script src="js/compile.js" />
<script src="js/index.js" />
<script type="text/javascript">
// 类似Vue的初始化模式
new SelfVue({
el: '#app',
data: {
title: 'hello world',
name: 'Sherwood'
},
methods: {
clickMe() {
this.title = 'fuck world'
}
},
mounted: function() {
window.setTimeout(() => this.title = '您好', 1000)
}
})
</script>
</html>

运行结果:



参考
Vue双向绑定是怎么实现的?
vue的双向绑定原理及实现
Vue 是如何实现数据双向绑定的?
SelfVue源码