Vue双向绑定原理梳理
简介
vue数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。
实现方式:数据劫持
结合 发布者-订阅者
模式。
数据劫持通过 Object.defineProperty()
方法。
对对象的劫持
- 构造一个监听器
Observer
,用来劫持并监听所有属性,添加到收集器Dep
中,当数据发生变化的时候发出一个notice
(通知)。 添加一个发布者
Dep
, 用来收集订阅者Watcher
和发布更新通知 。视图中会用到
data
中的key
,这称为依赖。同⼀个key
可能出现多次,每次都需要收集出来用⼀个Watcher
来维护它们,此过程称为依赖收集多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知。构造一个订阅者
Watcher
,一方面接收监听器Observer
通过Dep
传递过来的数据变化,一方面执行自身绑定的回调函数(update
)进行界面更新。- 实现一个解析器
Compile
,实现指令解析,初始化视图,并订阅数据变化,绑定好更新函数。值得注意的是,解析器在解析DOM的时候,是用的
DocumentFragment
,它是一个文档片段接口,表示一个没有父对象的最小文档对象,它的变化不会触发DOM树的重新渲染,性能优于直接操作DOM树。
Dep
如何收集 Watcher
Watcher
在取值的时候让 Dep.target
指向 this
即当前的 watcher
,取值结束之后,让 Dep.target
为 null
,这样就可以通过属性的 get
方法里面将当前的 watcher
添加到属性里面的实例 dep
中。1
2
3
4
5
6
7
8
9// 通过获取操作, 触发属性里面的get方法
key.split('.').reduce((total, current) => total[current], vm._data)
// get 方法
get: function getter () {
if (Dep.target) { // 在这里添加一个订阅者
dep.addSub(Dep.target);
}
return val;
}
对数组的劫持
由于数组不能直接使用 Object.defineProperty()
方法,所以它是这样操作的:
创建一个空对象继承Array的原型,然后创建了一个数组方法拦截器,在拦截器内重写了操作数组的一些方法(主要有7个:push
,pop
,shift
,unshift
,splice
,sort
,reverse
),当数组实例使用操作数组方法时,先触发拦截器中的方法使数组数据变成响应式数据。
数组数据变成响应式数据的方法,和前面对象的类似,主要是修改了监听器 Observer
,增加对数组的依赖收集和递归监听子元素,最后遍历数组,在循环内部如果有对象则进入对象的监听,触发依赖更新和监测新增元素通过拦截器。
vue 双向绑定原理简单模拟
以上说明可结合vue源码或下面简单模拟进行理解
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
<html>
<head>
<meta charset="utf-8">
<title>vue双向绑定原理</title>
</head>
<body>
<div id="app">
<input type="text" v-model="value">
<div>{{value}}</div>
<br>
<input type="text" v-model="data.input">
<div>{{data.input}}</div>
</div>
</body>
<script src="js/vue.index.js"></script>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
value: '输入框1',
data: {
input: '输入框2'
}
}
}
})
</script>
</html>
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
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203class Vue {
constructor(options) {
this.$options = options
const vm = this
if (this.$options.data) {
this.initData(vm)
}
if (this.$options.el) {
compile(this.$options.el, vm)
}
}
initData(vm) {
let data = vm.$options.data
data = typeof data === 'function' ? data.call(vm) : data
vm._data = data
observe(data)
for (let key in data) {
proxy(vm, key, data[key])
}
}
}
// 代理:实现 vm.name 可以直接访问 vm._data.name
function proxy(target, key, value) {
Object.defineProperty(target, key, {
get() {
return target['_data'][key]
},
set(newValue) {
target['_data'][key] = newValue
}
})
}
// 不是对象将被拦截
function observe(data) {
if (data === null || typeof data !== 'object') {
return
}
return new Observer(data)
}
// 监听器 这里只考虑对象了和对象嵌套, 没有考虑数组
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
// 遍历对象的每一项, 进行监听和劫持
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
}
}
// 通过defineProperty监听对象的属性并且给属性收集依赖
function defineReactive(target, key, value) {
// 递归监听属性值是对象的属性
observe(value)
// 添加Dep实例, 收集依赖
let dep = new Dep()
Object.defineProperty(target, key, {
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue) {
value = newValue
// 给新值添加监听
observe(newValue)
// 修改值的时候通知订阅者Watcher去更新
dep.notify()
}
})
}
// 收集器 - 依赖收集
class Dep {
constructor() {
// 里面装的是收集的watcher
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
// 让收集到的所有watcher去更新
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
// 订阅者
class Watcher {
constructor(vm, key, callback) {
this.vm = vm
this.key = key
this.callback = callback
// 让 Dep.target 属性指向当前 watcher 实例
Dep.target = this
// reduce 逐个遍历数组元素, 每一步都将当前元素的值与上一步的计算结果相加
// 通过 reduce, 触发 defineReactive 的 get 方法, 从而让 Dep 收集到
key.split('.').reduce((total, current) => total[current], vm._data)
Dep.target = null
}
update() {
const value = this.key.split('.').reduce((total, current) => total[current], this.vm._data)
this.callback(value)
}
}
/** 解析模板
使用 documentFragment 创建模板, 注意 fragment.append 会让被插入的 child 节点从父节点中移除, while 循环结束后, 页面就没了
然后对模板里面的每一项进行解析:
先实例 node.nodeType === 3 的元素, 表示文本节点, 看文本节点里面有没有匹配到 {{name}} 模板表达式的,
如果有, 从 vm._data 里面去取对应的值, 替换文本的值, 最后 vm.$el.appendChild(fragment) 就可以将替换后的结果显示在页面上
对 nodeType === 1 的元素, 即标签解析, 这里我们假设处理的是 input,
获取节点的所有属性, 一个伪数组, 变成真数组, 里面有个 nodeName === v-model 和 nodeValue 对应 name 的,
同样获取 vm._data 里面 name 的值, 然后让节点的 node.value = 这个值, 就能显示在输入框里面了, 这就是数据改变视图。
接下来是视图改变数据, 添加 input 方法, 为 node 添加 addEventListener方法, input, 然后让 vm._data 里面对应属性的值等于 e.target.value, 这样就实现了视图改变数据。
重点: 上面的两种情况, nodeType == 3 的时候更新方法是 node.nodeValue = newValue, nodeType == 1 的时候更新方法是 node.value = newValue,
需要将这两个方法封装到 watcher 中, 在更新之后 new 一个 Watcher, 并将对应的参数传入, 后面在获取值的时候就会自动收集依赖, set 值的时候就会触发更新。
* @param {Object} el
* @param {Object} vm
*/
function compile(el, vm) {
vm.$el = el = document.querySelector(el)
const fragment = document.createDocumentFragment()
let child
while (child = el.firstChild) {
fragment.append(child)
}
fragment_compile(fragment)
function fragment_compile(node) {
const parttern = /\{\{\s*(\S+)\s*\}\}/
// 文本节点
if (node.nodeType === 3) {
// 匹配 {{}}, 第一项为匹配的内容, 第二项为匹配的变量名称
const match = parttern.exec(node.nodeValue)
if (match) {
const needChangeValue = node.nodeValue
// 获取到匹配的内容, 可能是 msg, 也可能是 mmm.msg,
// 注意通过 vm[mmm.msg] 是拿不到数据的, 要 vm[mmm][msg]
// 获取真实的值, 替换掉模板里面的 {{name}}, 真实的值从 vm.$options.data 里面取
let arr = match[1].split('.')
let value = arr.reduce(
(total, current) => total[current], vm._data
)
// 将真实的值替换掉模板字符串, 这个就是更新模板的方法, 将这个方法封装到 watcher 里面
node.nodeValue = needChangeValue.replace(parttern, value)
const updateFn = value => {
node.nodeValue = needChangeValue.replace(parttern, value)
}
// 有个问题, node.nodeValue 在执行过一次之后, 值就变了, 不是 {{name}}, 而是真实值, 要将 {{name}} 里面的 name(即match[1]) 暂存起来
new Watcher(vm, match[1], updateFn)
}
return
}
// 元素节点
if (node.nodeType === 1 && node.nodeName === 'INPUT') {
// 伪数组
const attrs = node.attributes
let attr = Array.prototype.slice.call(attrs)
// 里面有个 nodeName === v-model, 有个 nodeValue 对应 name
attr.forEach(item => {
if (item.nodeName === 'v-model') {
let value = getVmValue(item.nodeValue, vm)
// input 标签是修改 node.value
node.value = value
// 也需要添加 watcher
new Watcher(vm, item.nodeValue, newValue => node.value = newValue)
// 添加 input 事件
node.addEventListener('input', e => {
const name = item.nodeValue
// 给 vm 上的属性赋值
// 不能直接 vm._data[name] = e.target.value , 因为 name 可能是 a.b 的形式
// 也不能直接获取 b 的值, 然后赋新值, 因为这个值是一个值类型, 需要先获取前面的引用类型
// 如: let tem = vm._data.a, 然后 tem[b] = 新值, 这样就可以达到 vm._data.a.b = 新值的效果
const arr1 = name.split('.')
const arr2 = arr1.slice(0, arr1.length - 1)
const head = arr2.reduce((total, current) => total[current], vm._data)
head[arr1[arr1.length - 1]] = e.target.value
})
}
})
}
node.childNodes.forEach(child => fragment_compile(child))
}
vm.$el.appendChild(fragment)
}
function getVmValue(key, vm) {
return key.split('.').reduce((total, current) => total[current], vm._data)
}
function setVmValue(key, vm) {
let tem = key.split('.')
let fin = tem.reduce((total, current) => total[current], vm._data)
return fin
}
window.Vue = Vue;