vue原理


VUE响应式原理

核心是通过数据劫持和观察者模式(发布者-订阅者模式)实现的.
简单实现:

  • 观察者Observe
    • 递归对一个实例的每一个属性值都进行观察
    • 响应式监听属性的函数defineReactive
      • 实例化Dep订阅器
      • Object.defineProperty() 实现数据劫持(getter,setter)
        • getter中通过dep添加Watcher订阅者
        • setter中通过dep.notify()发布更新通知
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
// Observer.js
class Observer {
constructor (data) {
this.data = data // 创建Vue实例时初始化数据
this.walk(data) // 递归劫持数据的每个属性
}
walk (data) {
if (!data || typeof data !== 'object') return
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
this.walk(data[key])
})
}
defineReactive (obj, key, val) {
let dep = new Dep() // watcher的订阅器,实例化dep,确保个数据属性独立监听
Object.defineProperty(obj, key, {
enumerable:true,
configurable: true,
get () {
// 读取数据时应在适当时机添加watcher,
Dep.target && dep.addSubs(Dep.target) // Dep.target就是watcher的实例,
return val
},
set (newVal) {
if (val === newVal) return
val = newVal
this.walk(newVal) // 新值为对象时需要深层劫持
dep.notify() // 触发dep通知,让watcher触发update()更新视图
}
})
}
}
  • 订阅器Dep
    • 维护订阅者(Watcher)数组
    • 数据更新时广播给订阅者,触发订阅者更新方法
    • 在vue中,notiy并不是直接触发更新,而是通过queueWatcher异步队列更新(一般基于promise实现),$nextTick就是表示当前更新任务结束后的一个钩子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// watcher.js
class Dep {
constructor () {
this.subs = [] // watcher数组
}
addSubs (watcher) {
this.subs.push(watcher)
}
notify () {
this.subs.forEach(sub => {
sub.update() // 更新方法,watcher的方法,watcher在compile中实例化,与vDom关联,因此能实现model-->view的更新
})
}
}
  • 订阅者Watcher
    • update方法更新数据
    • get方法触发Observe中getter,将自己添加
    • vue2中watcher对应每个组件,不是每个数据,首先初始化后在mountComponent中为跟组件创建watcher,之后每个新组建创建时都会创建对应watcher,单独通过$watch监听的数据会产生新的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
// watcher.js
class Watcher {
constructor (vm, expr, callback) {
this.vm = vm // Vue实例,用于获取data
this.expr = expr // 数据属性,如data.msg中的msg
this.callback = callback // 更新数据后的回调,在compile中触发,实现vnode的更新
Dep.target = this // 实例化时将自己的实例付给Dep.target
this.oldVal = this.getVmData() // 将旧值记录,update时对比
// 此过程触发Observer中getter,实现将watcher添加到dep中
// 注意: watcher是实例化自己时才添加到dep中的,而watcher的实例化是在compile中触发的
Dep.target = null // 完成添加watcher后置空
}
getVmData () { // 获取vue实例中data中某个属性数据
let val = this.vm.$data[this.expr]
return val
}
update () {
let newVal = this.getVmData() // 此时vue实例data已经改变,所以拿到的是新值
if (newVal !== this.oldVal) {
this.callback(newVal, this.oldVal)
this.oldVal = newVal
}
}
}
  • 解析器compile
    • 递归遍历node解析指令(v-model,双花括号等)
    • 监听node的input实现view => model
    • 实例化watcher,watcher的update方法触发时,更新view,实现model => view
    • 创建’虚拟’(碎片化文档)dom方法node2Fragment
      • DocuemntFragment(碎片化文档)
      • 调用compile
      • 生成碎片化文档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
// compile.js
class Compile {
constructor (el, vm) {
this.el = typeof el === 'string' ? document.querySelect(el) : el
this.vm = vm
if (el) {
let fragment = this.node2Fragment(this.el)
this.compileFragment(fragment)
this.el.appendChild(fragment)
}
}
node2Fragment (el) {
let fragment = document.createDocumentFragment() // 碎片化文档
let childNodes = el.childNodes // #app下的子元素
this.toArray(childNodes).ferEach(child => { // childNodes需要处理数组格式
fragment.appendChild(child) // 将页面#app下所有元素添加到碎片化文档
})
return fragment
}
compileFragment (fragment) { // 此方法包含初始化解析指令和实例化watcher实现监听这两个过程
let childNodes = fragment.childNodes
this.toArray(childNodes).ferEach(node => {
if (this.isElementNode(node)) { // 如果是元素节点
this.compileElementNode(node)
}
if (this.isTextNode(node)) { // 如果是文本节点
this.compileTextNode(node)
}
if (node.childNodes && node.childNodes.length) {
this.compileFragment(node) // 递归每个节点
}
})
}
toArray (nodes) {
return [].slice.call(nodes)
}
isElementNode (node) {
return node.nodeType === 1
}
isTextNode (node) {
return node.nodeType === 3
}
compileElementNode (node) { // 元素的化就需要解析v-model等指令
let attrs = node.attributes
this.toArray(attrs).forEach(attr => {
if (attr.nodeName === 'v-model') { // 逐个指令解析
let expr = attr.nodeValue
node.value = this.vm.$data[expr] // 初始化指令赋值操作
node.addEventListener('input', function () { // 监听input事件,实现view-->model
that.vm.$data[expr] = this.value
})
new Watcher(this.vm, expr, newVal => {
node.value = newVal
})
}
})
}
compileTextNode (node) {
let reg = /\{\{(.*)\}\}/
if (reg.test(node.nodeValue)) {
let expr = RegExp.$1
node.textContent = this.vm.$data[expr] // 初始化解析{{}}时
new Watcher (this.vm, expr, newVal => { // 实例化watcher,回调为更改node值,实现model-->view
node.textContent = newVal
})
}
}
}
  • Vue构造函数
    • 双向数据绑定入口
    • 实例化,调用Observer和virtualDom
    • 将碎片化文档dom添加到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
// Vue.js
class Vue {
constructor (options = {}) {
this.$data = options.data
this.$el = options.el
this.$methods = options.methods
this.proxyData(this.$data)
this.proxyData(this.$methods)
new Observer(this.$data)
if (el) {
new Compile(this.$el, this)
}
}
proxyData (data) {
if (!data) return
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get () {
return data[key]
},
set (val) {
if (data[key] === val) return
data[key] = val
}
})
})
}
}

完整代码

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
<body>
<div id="app">
<input type="text" v-model="msg">
<div>{{msg}}</div>
</div>

<script name="Watcher">
class Watcher {
constructor (vm, expr, callback) {
// debugger
this.vm = vm
this.expr = expr
this.callback = callback
// debugger
Dep.target = this
// debugger
this.oldVal = this.getVmData()
// debugger
Dep.target = null
// debugger
}
getVmData () {
let val = this.vm.$data[this.expr]
return val
}
update () {
let newVal = this.getVmData()
if (this.oldVal !== newVal) {
this.callback(newVal, this.oldVal)
this.oldVal = newVal
}
}
}

class Dep {
constructor () {
// debugger
this.subs = []
}
addSubs (watcher) {
this.subs.push(watcher)
}
notify () {
this.subs.forEach(sub => {
sub.update()
})
}
}
</script>
<script name="Observer">
class Observer {
constructor (data) {
// debugger
this.data = data
this.walk(data)
}
walk (data) {
if (!data || typeof data !== "object") return
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
this.walk(data[key])
})
}
defineReactive (obj, key, val) {
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable:true,
configurable: true,
get: function () {
// debugger
Dep.target && dep.addSubs(Dep.target)
return val
},
set: (newVal) => {
if (val === newVal) return
val = newVal
this.walk(newVal)
dep.notify()
}
})
}
}
</script>
<script name="Compile">
class Compile {
constructor (el, vm) {
// debugger
this.el = typeof el === 'string' ? document.querySelector(el) : el
this.vm = vm
if (el) {
let fragment = this.node2fragment(this.el)
this.compileFragment(fragment)
this.el.appendChild(fragment)
}
}
node2fragment (el) {
let fragment = document.createDocumentFragment()
let childNodes = el.childNodes
this.toArray(childNodes).forEach(child => {
fragment.appendChild(child)
})
return fragment
}
compileFragment (fragment) {
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node => {
if (this.isElementNode(node)) {
this.compileElementNode(node)
}
if (this.isTextNode(node)) {
this.compileTextNode(node)
}
if (node.childNodes && node.childNodes.length) {
this.compileFragment(node)
}
})
}
isElementNode (node) {
return node.nodeType === 1
}
isTextNode (node) {
return node.nodeType === 3
}
compileElementNode (node) {
let that = this
let attrs = node.attributes
this.toArray(attrs).forEach(attr => {
if (attr.nodeName === 'v-model') {
let expr = attr.nodeValue
node.value = this.vm.$data[expr]
node.addEventListener('input', function () {
that.vm.$data[expr] = this.value
})
new Watcher(this.vm, expr, newVal => {
node.value = newVal
})
}
})
}
compileTextNode (node) {
let reg = /\{\{(.*)\}\}/
if (reg.test(node.nodeValue)) {
let expr = RegExp.$1
node.textContent = this.vm.$data[expr]
new Watcher(this.vm, expr, newVal => {
node.textContent = newVal
})
}
}
toArray(classArray) {
return [].slice.call(classArray)
}
}
</script>
<script name="Vue">
class Vue {
constructor (options = {}) {
this.$data = options.data
this.$methods = options.methods
this.$el = options.el
// debugger
this.proxyData(this.$data)
this.proxyData(this.$methods)
new Observer(this.$data)
if (this.$el) {
new Compile(this.$el, this)
}
}
proxyData (data) {
// debugger
if (!data) return
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get () {
return data[key]
},
set (v) {
if (data[key] === v) return
data[key] = v
// return v
}
})
})
}
}
</script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'hi'
}
})
setTimeout(() => {
vm.msg = 'hhh'
}, 1000);
</script>
</body>

Vue3原理篇

因为 Object.definePorperty 只能对 对象的已知属性 进行操作,所有才会导致 对象新增、删除属性时无法触发视图更新,需要通过魔法($set)来处理。而数组也因为是通过重写数组方法和遍历数组元素进行的响应式处理,也会导致按照数组下标进行赋值或者更改元素时无法触发视图更新。
因此vue3采用了全新的 Proxy 对象来实现整个响应式系统基础,仅将 getter / setter 用于 ref。

与defineProperty相比
• 拦截操作更加多样
• 拦截定义更加直接
• 性能更加高效



参考:

手把手教你撸一个vue框架(原理篇)
Vue 响应式原理模拟
理解VUE双向数据绑定原理和实现
深入响应式原理
Vue 3 核心原理 – reactivity 自己实现
Vue3 响应式原理
浅析Vue3中的响应式原理