浅析vue的响应式原理
2024-12-09 11:13:16

面试官:您能详细说说vue的双向绑定原理吗?

我:双向绑定是v-model的实现方式,我猜您问的不是那么简单的问题,也许您是想说vue的响应式原理?

面试官:对对,就是这个,您说一下Vue的双向绑定原理?

我:哈哈哈,我不会。

面试官:哈哈哈,您真幽默,和您聊得很开心,HR后续会通知您面试结果的。

正文

因为最近找工作,面试vue3相关的知识点多一些,所以这里更多看

本来想考虑自己看源码写一份文档的,但是同事推荐了几篇文档之后,我觉得自己还是不要出来丢人了。

看了市面上几位大佬的解析之后,我觉得自己的理解多少还是菜了些。

这里我就转载其中一篇文档,我个人倾向于vue响应式详解(重学前端-vue篇1) - 掘金 (juejin.cn)这篇文档,因为我个人基础相对来说还可以,而且看了vue3的源码,所以这里更喜欢笼统的看个大概得思路,这样不乱。

不过,如果是新人,我更推荐看0年前端的Vue响应式原理学习总结1:基本原理 - 掘金 (juejin.cn),这篇文档写的会连带着实现动机解释的很明白,如果你之前没有看过VUE的源码,那么这篇文档能让新手非常顺滑的理解vue实现的动机和过程。

vue的原理确实不麻烦,但是思路很精妙,我们平日大概率是用不上的,所以这里只是拓充知识。

毕竟,大多数人不是底层的源码实现者,所以只能学习思路,至于平日代码实现,如果有机会实现,那确实厉害。

什么是Vue响应式

数据发生变化后,会重新对页面渲染,这就是Vue响应式

image-20240531232228283

想完成这个过程,我们需要做些什么

  • 侦测数据的变化
  • 收集视图依赖了哪些数据
  • 数据变化时,自动“通知”需要更新的视图部分,并进行更新

「它们对应专业俗语分别是:」

  • 数据劫持 / 数据代理
  • 依赖收集
  • 发布订阅模式

什么是发布订阅模式

  • 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的
  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

image-20240531232458202

vue响应式采用的就是发布订阅模式,举个生活中的例子说明

1
2
3
4
5
比如小红最近在淘宝网上看上一双鞋子,但是联系到卖家后,才发现这双鞋卖光了。
但是小红对这双鞋又非常喜欢,所以联系卖家,问卖家什么时候有货。
卖家告诉她,要等一个星期后才有货,卖家告诉小红,要是你喜欢的话,你可以收藏我们的店铺,等有货的时候再通知你。
所以小红收藏了此店铺。
但与此同时,小明,小花等也喜欢这双鞋,也收藏了该店铺,等来货的时候就依次会通知他们;

如何实现发布–订阅模式?

  1. 首先要想好谁是发布者(比如上面的卖家)。
  2. 然后给发布者添加一个缓存列表,用于存放回调函数来通知订阅者(比如上面的买家收藏了卖家的店铺,卖家通过收藏了该店铺的一个列表名单)。
  3. 最后就是发布消息,发布者遍历这个缓存列表,依次触发里面存放的订阅者回调函数

如何侦测数据的变化

有两种办法可以侦测到变化:
使用Object.defineProperty和ES6的Proxy,这就是进行数据劫持或数据代理。

Object.defineProperty实现

Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

1
2
3
4
5
6
7
8
9
10
function render () {
// set的时候会走这里,重新渲染
console.log('模拟视图渲染')
}
let data = {
name: '浪里行舟',
location: { x: 100, y: 100 }
}
// 请看下一节
observe(data)

定义核心函数observe

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
function observe (obj) { // 我们来用它使对象变成可观察的
/**
判断类型,obj可能是vue中的data对象,也可以是data中的某个属性值,
例如:data:{ name: '浪里行舟', location:{ x: 100, y: 100 } },这里的值可能是整个data,
也可能是location的值,当location值也是对象的时候就需要再次监听location值对象。
而属性name和location在监听data的时候就会监听这2个属性。
*/
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})

//监听对象的每个属性
function defineReactive (obj, key, value) {
// 递归子属性
observe(value)
Object.defineProperty(obj, key, {
enumerable: true, //可枚举(可以遍历)
configurable: true, //可配置(比如可以删除)
get: function reactiveGetter () {
console.log('get', value) // 监听
return value
},
set: function reactiveSetter (newVal) {
observe(newVal) //如果赋值是一个对象,也要递归子属性
//如果有新值
if (newVal !== value) {
console.log('set', newVal) // 监听
render() //执行渲染逻辑
value = newVal
}
}
})
}
}

改变data的属性,会触发set;然后获取data的属性,会触发get

1
2
3
4
5
data.location = {
x: 1000,
y: 1000
} //打印=》 set {x: 1000,y: 1000} 染
data.name //打印=》 get 浪里行舟

上面这段代码的主要作用

observe这个函数传入一个 obj(需要被追踪变化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理,给每个属性加上setget方法,以此来达到实现侦测对象变化。

值得注意的是,observe 会进行递归调用,因此我们需要设置一个递归出口。

那我们如何侦测Vue中data 中的数据,其实也很简单

1
2
3
4
5
6
7
class Vue {
/* Vue构造类 */
constructor(options) {
this._data = options.data;
observer(this._data);
}
}

这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行追踪变化。

但是我们发现一个问题,上面的代码无法检测到对象属性的添加或删除(如data.location.a=1,增加一个a属性)。

这是因为 Vue 通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。

如果是删除属性,我们可以用vm.$delete实现,那如果是新增属性,该怎么办呢?

  1. 可以使用 Vue.set(location, a, 1) 方法向嵌套对象添加响应式属性;
  2. 也可以给这个对象重新赋值,比如data.location = {…data.location,a:1}

Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写

观察者 Watcher

为什么引入Watcher

Vue 中定义一个 Watcher 类来表示观察订阅依赖

至于为啥引入Watcher,《深入浅出vue.js》给出了很好的解释。

属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。

然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。

「依赖收集的目的」 将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中,形成如下所示的这样一个关系(图参考《剖析 Vue.js 内部运行机制》)。

image-20240602230312707

Watcher的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
this.cb(this.value)
}
}

以上就是 Watcher 的简单实现,在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher ,然后执行 update 函数。

「依赖的本质」

所谓的依赖,其实就是Watcher

至于如何收集依赖,总结起来就一句话:

getter中收集依赖(收集Watch当如Dep中),在setter中触发依赖。

先收集依赖,即把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行了。

具体来说,当外界通过Watcher读取数据时,便会触发getter从而将Watcher添加到依赖中,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。

当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。

最后我们对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码,实现了一个简易的数据响应式:

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
function observe (obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
function defineReactive (obj, key, value) {
observe(value) // 递归子属性
let dp = new Dep() //新增
Object.defineProperty(obj, key, {
enumerable: true, //可枚举(可以遍历)
configurable: true, //可配置(比如可以删除)
get: function reactiveGetter () {
console.log('get', value) // 监听
// 将 Watcher 添加到订阅
// target挂载发生在new Watch的时候
if (Dep.target) {
// 新增Watch到dep的subs中,就是说new了哪个Watch就要把哪个Watch收集进来
dp.addSub(Dep.target)
}
return value
},
set: function reactiveSetter (newVal) {
// 如果赋值是一个对象,也要递归子属性
// 例如上面的location是一个对象,那么这个对象是要继续observe的
observe(newVal)
if (newVal !== value) {
console.log('set', newVal) // 监听
render()
value = newVal
// 执行 watcher 的 update 方法
dp.notify() //新增
}
}
})
}
}

class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
new Watcher();
console.log('模拟视图渲染');
}
}

当 render function 被渲染的时候,读取所需对象的值,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。

之后如果修改对象的值,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

「完整流程图」

image-20240602230619901

  • 在 new Vue() 后, Vue 会调用_init 函数进行初始化,也就是init 过程,在 这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行getter 函数,而在当被赋值的时候会执行 setter函数。
  • 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
  • 在修改对象的值的时候,会触发对应的setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。

收集依赖

为什么要收集依赖

我们之所以要观察数据,其目的在于当数据的属性发生变化时,可以通知那些曾经使用了该数据的地方。比如例子中,模板中使用了location 数据,当它发生变化时,要向使用了它的地方发送通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let globalData = {
text: '浪里行舟'
};
let test1 = new Vue({
template:
`<div>
<span>{{text}}</span>
<div>`,
data: globalData
});
let test2 = new Vue({
template:
`<div>
<span>{{text}}</span>
<div>`,
data: globalData
});

如果我们执行下面这条语句:

1
globalData.text = '前端工匠';

此时我们需要通知 test1 以及 test2 这两个Vue实例进行视图的更新,我们只有通过收集依赖才能知道哪些地方依赖我的数据,以及数据更新时派发更新。那依赖收集是如何实现的?其中的核心思想就是“事件发布订阅模式”。接下来我们先介绍两个重要角色– 订阅者 Dep观察者 Watcher ,然后阐述收集依赖的如何实现的。

Dep,Watcher的关系

Observer负责将数据转换成getter/setter形式; Dep负责管理数据的依赖列表;是一个发布订阅模式,上游对接Observer,下游对接Watcher Watcher是实际上的数据依赖,负责将数据的变化转发到外界(渲染、回调); 首先将data传入Observer转成getter/setter形式;当Watcher实例读取数据时,会触发getter,被收集到Dep仓库中;当数据更新时,触发setter,通知Dep仓库中的所有Watcher实例更新,Watcher实例负责通知外界

  • Dep 负责收集所有相关的的订阅者 Watcher ,具体谁不用管,具体有多少也不用管,只需要根据 target 指向的计算去收集订阅其消息的 Watcher 即可,然后做好消息发布 notify 即可。
  • Watcher 负责订阅 Dep ,并在订阅的时候让 Dep 进行收集,接收到 Dep 发布的消息时,做好其 update 操作即可。

Vue的操作就是加入了发布订阅模式,结合Object.defineProperty的劫持能力,实现了可用性很高的双向绑定。

订阅器 Dep

image-20240606000301782

「为什么引入 Dep」
收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖删除依赖向依赖发送消息等。
于是我们先来实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,「说得具体点」:它的主要作用是用来存放 Watcher 观察者对象。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方

「Dep的简单实现」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Dep {
constructor () {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 在subs中添加一个Watcher对象 */
addSub (sub) {
this.subs.push(sub);
}
/* 通知所有Watcher对象更新视图 */
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}

以上代码主要做两件事情:

  • 用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
  • 用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。 所以当需要依赖收集的时候调用 addSub,当需要派发更新的时候调用 notify。

也就说,所有的Watch最终会存放到Dep的subs中,并且视图更新的流程是,Dep触发了subs中的每个Watch,执行了Watch更新逻辑,Dep的操作是绕不过Watch的,这就验证了上面说的,Watch是一个中介的角色。

调用也很简单:

1
2
3
4
5
let dp = new Dep()
dp.addSub(() => {//依赖收集的时候
console.log('emit here')
})
dp.notify()//派发更新的时候

实现

先实现new 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
//遍历data,进行数据劫持
function Observer(data) {
Object.keys(data).forEach((item) => {
defineReactive(data, item, data[item])
})
}

function defineReactive(data, key, val) {
const self = this
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
return val
},
set(newVal) {
if (newVal === val) {
return
}
val = newVal
},
})
}

class Vue {
constructor(options) {
if (options && typeof options.data == 'function') {
//模拟vue源码,挂载data到Vue的_data上
this._data = options.data.apply(this)
}
// 劫持传入的data项
new Observer(this._data)
}
}

const vue = new Vue({
data() {
return {
text: 'hello',
}
},
})
console.log(vue)//data中属性已经被劫持了

劫持以后,vue.mount()关联Watch

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
js复制代码//遍历data,进行数据拦截
function Observer(data) {
Object.keys(data).forEach((item) => {
defineReactive(data, item, data[item])
})
}

function defineReactive(data, key, val) {
const self = this
Object.defineProperty(data, key, {
enumerable: true,
// 设置当前描述属性可被修改
configurable: true,
get() {
return val
},
set(newVal) {
if (newVal === val) {
return
}
val = newVal
},
})
}

class Watcher {
//----------4
constructor(vm, fn) {
this.vm = vm //----------5
fn() //----------6
}
}

class Vue {
constructor(options) {
if (options && typeof options.data == 'function') {
this._data = options.data.apply(this)
}
new Observer(this._data)
}
mount = () => {
//--------------2
new Watcher(this, this.render) //--------------3
}
render = () => {
// 省略一系列的渲染逻辑
return this._data.text //---------7
}
}
const vue = new Vue({
data() {
return {
text: 'hello',
}
},
})
vue.mount() //------------ 1

Vue mount函数的功能是去读取data中的属性渲染到页面上。

这里的关键一步是读数据

那么,读数据最终是要进入数据劫持get方法的。这时我们可以创建一个Watch(观察者)作为中介,让它去进入get。

而mount起到的作用是渲染,自然,在构造Watch的时候需要传入渲染函数。

回到前面我们说的,Watch充当的是中介角色,那么,它是谁和谁之前的中介?实际上它是Observer和我们后面要讲的Dep的中介。

既然是中介,那么它就需要拿到双方的数据。又因为Observer需要观察data,所以这里我们需要将整个Vue构造函数挂载到Watch自身。

整理下流程:

执行vue.mount()渲染页面,需要在Vue定义渲染函数render,然后在mount通过中介Watch去执行定义在Vue的render函数。

创建中介Watch,执行传入的渲染函数,渲染肯定是需要根据页面绑定了哪些data属性去读取data的,这个时候就会触发get方法,

进入get拿到对应的值。

添加dep后大概结构

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
//defineReactive是对Observer的抽离
const defineReactive = function(obj, key) {
// 以下代码省略
}

const Vue = function(options) {
console.log("Vue",this)
//打印1 Vue {
_data:{
text: "123"
get text: ƒ get()
set text: ƒ set(newVal)
},
mount: ƒ (),
render: ƒ ()
}
// 以下代码省略
}

const Watcher = function(vm, fn) {
console.log("Watcher",this)
//打印3 Watcher this是下面的Dep中subs的对象
// 以下代码省略
}

const Dep = function() {
console.log("Dep",this)
//打印2 Dep {
target: null,
subs: [
{ //是一个Watcher实例
subs: Array(1)
0: Watcher
vm: { //是一个Vue实例
_data:{
text: "123",//该属性有了get和set方法
get text: ƒ get(),
set text: ƒ set(newVal)
},
mount: ƒ (),
render: ƒ ()
},

addDep: ƒ (dep),
update: ƒ (),
value: undefined
}
],
depend: ƒ (),
addSub: ƒ (watcher),
notify: ƒ ()
}

// 以下代码省略
}

const vue = new Vue({
data() {
return {
text: 'hello world'
};
}
})

vue.mount();
vue._data.text = '123';

详细代码

参考这个图,会了看的更清晰,注意看执行顺序,在new Vue的时候,就会按顺序,初始化各个构造函数

image-20240606000442133

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
/**
*
* 作用:
* 劫持data的各个属性,挂载set和get方法
* 在get中将Watch添加到dep的subs中
* 在set中触发dep.notify更新视图
*/
const Observer = function (data) {
console.log(1) //开始4 new Vue的时候就会执行
// 循环修改为每个属性添加get set
for (let key in data) {
defineReactive(data, key);
}
}

const defineReactive = function (obj, key) {
console.log(2) //开始5 new Vue的时候就会执行
// 局部变量dep,用于get set内部调用
const dep = new Dep();
// 获取当前值
let val = obj[key];
Object.defineProperty(obj, key, {
// 设置当前描述属性为可被循环
enumerable: true,
// 设置当前描述属性可被修改
configurable: true,
get() {
console.log(3)//开始10 开始19
console.log('in get');
// 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
dep.depend();
return val;
},
set(newVal) {
console.log(4)//开始15
if (newVal === val) {
return;
}
val = newVal;
// 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
// 这里每个需要更新通过什么断定?dep.subs
dep.notify();
}
});
}

const Vue = function (options) {
console.log(6)//开始1 new Vue的时候就会执行
const self = this;
// 将data赋值给this._data,源码这部分用的Proxy,这里我们用最简单的方式临时实现
if (options && typeof options.data === 'function') {
console.log(7)//开始2 options.data是个函数,它返回了一个对象
this._data = options.data.apply(this);
}
// 挂载Dom函数
this.mount = function () {
console.log(8) //开始7 new Vue以后,执行vue.mount()
new Watcher(self, self.render);
}
// 渲染函数
this.render = function () {
console.log(9) //开始9 开始18 render函数执行后走到这里
return self._data.text; //这里取data值的时候,就会走get方法
}
// 开始3, 监听this._data
//new Vue的时候就会执行,这里执行完,就表示new Vue的过程执行完了
new Observer(this._data);
}

const Watcher = function (vm, fn) {
console.log(10) //开始8 执行vue.mount()以后会走到这里
const self = this;
this.vm = vm;
// 将当前Dep.target指向自己
// 每次执行new Watch的时候,都会把当前的Watcher挂载到Dep.target
Dep.target = this;
// 向Dep方法添加当前Wathcer
// this.addDep = function (dep) {
// console.log(11) //开始13
// dep.addSub(self);
// }
// 更新方法,用于触发vm._render
this.update = function () {
console.log(12)//开始17
console.log('in watcher update');
fn(); //Vue中的render函数
}
// 这里会首次调用vm._render,从而触发text的get
// 从而将当前的Wathcer与Dep关联起来
this.value = fn(); //开始9 fn是Vue中的render函数,这里fn()在赋值的时候会执行
// 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
// 造成代码死循环
console.log('Watcher', this)
Dep.target = null;
}

const Dep = function () {
//开始6 new Vue》Observer》defineReactive》new Dep()
console.log(13)
const self = this;
// 收集目标,先把它置空
this.target = null;
// 存储收集器中需要通知的Watcher
this.subs = [];
// 当有目标时,绑定Dep与Wathcer的关系

this.depend = function () {
console.log(14) //开始11 开始20 走了get获取属性后,就要进行依赖收集
// targrt挂载发生在new Watch的时候
if (Dep.target) {
console.log(15)//开始12
self.addSub(Dep.target)
}
}
// 为当前收集器添加Watcher
this.addSub = function (watcher) {
console.log(16)//开始14
self.subs.push(watcher);
}
// 通知收集器中所的所有Wathcer,调用其update方法
this.notify = function () {
console.log(17) //开始16
for (let i = 0; i < self.subs.length; i += 1) {
self.subs[i].update();
}
}
}

const vue = new Vue({
data() {
return {
text: 'hello world'
};
}
})

vue.mount(); // 挂载dom,渲染页面
vue._data.text = '123'; // 修改属性值,触发set

「解析:」

  • 一开始new Vue ,会走到46行执行Vue构造函数,打印6,然后判断传入的是否是个函数,从new Vue可以看出,传入的是个data()函数,然后将函数执行后返回的整个data对象挂载到vue的_data属性上(真实的vue也是这么操作的)
  • 接着依次挂载mountrender函数。
  • 然后 new Observer(this._data);
  • new Observer 的时候会走到第一行Observer(关键函数),打印1。我们发现Observer实际就是给data数据都添加上get和set方法,只不过不添加的方法defineReactive给抽离出去了。
  • 然后走到第9行,执行defineReactive,打印2。 defineReactive的作用:
    • 劫持data的各个属性,挂载setget方法
    • get中将Watch添加到depsubs
    • set中触发dep.notify更新视图
  • 然后走到12行,new Dep的时候,会走到95行执行Dep,打印13。每次new Dep 的时候,都会置空target,this.target = null,Dep函数剩下的代码都只是定义函数depend和addSub,notify,都不会执行,会跳出Dep函数。然后会到defineReactive函数第13行。
  • 然后15行给每个属性加上get和set方法。注意:此时只是在挂载,还没有执行,因此不会进入get,set方法内部。也就是说defineReactive剩下的代码中的函数也不会执行,所以会回到Observer,再回到67行new Observer,即new Vue的过程走完了

new Vue执行完毕

开始执行vue.mount()

  • 然后走到135行的vue.mount(),走到56行,打印8。执行new Watcher,进入Watcher构造函数打印10,然后this.vm = vm; ,将vue实例挂载到Watch的vm属性上了。Dep.target = this,将watch实例挂载到了Dep的target属性上,从而关联起来。然后挂载addDep和update方法,只是定义,没有执行。
  • 接着89行this.value = fn():fn实际是传进来的render函数(Vue=>new Watcher(self, self.render)=>vm, fn),由于后面有(),所以会立即执行。然后走到60行的render函数,打印9,返回 self._data.text,然后回到Watch中this.value就是返回值data.text。然后,关键的来了:self._data.text这里读取了data中的text,那么,这一步就会触发get方法。
  • 然后走到21行的get,打印3。
  • 然后走到25行,执行dep.depend(),再走到104行,打印14。
  • 这时候判断Dep.target,由于第8步将watch挂载到了Dep.target,这时候为true,所以打印15。然后执行Dep.target.addDep(self),其实就是执行Watch.addDep(self),然后执行self.addSub(Dep.target)
  • 然后进入this.addSub打印16,完成了依赖收集,subs中就有了Watch了。然后会回到get,执行最后一行,退出get,接着回到this.render,退出render函数,接着回到Watch中的this.value = fn(),继续往后走,Dep.target = null,避免陷入死循环,然后Watch执行完了。

vue.mount()也执行完毕

开始执行修改操作

  • 136行赋值操作了vue._data.text = '123',这时候会走到28行的set,打印4,然后设置值为新值
  • 继续向下走,到36行,dep.notify(),然后走到119行,打印17。
  • 会走到122行,遍历Depsubs数组里面所有的Watch,触发Watch的update方法,走到82行,打印12。
  • 执行Watcher中的fn(),即Vue中的render函数(new Watcher(self, self.render)时传入的render函数),走到60行,打印9。render函数中其实就可以做一些渲染的操作,例如:获取某个节点,将他的内容变成获取的值,就完成了页面的渲染。
  • 走到63行,取data.text,会走get,走到21,打印3。
  • 执行dep.depend(),走到Dep中的this.depend,打印14。但是由于Dep.target为null,15不会打印,也就是说到了这里,所有的流程执行完了。

全部执行完毕

结语

响应式原理,大龄前端确实应该掌握的必需品,如今我才看确实有点晚。

不过,学习任何时候都不算晚,只是当初看着晦涩难懂的代码如今看起来居然如鱼得水,也真是奇怪。

而且,这并非工程化的核心,如果涉及到基础开发才会需要了解,平时我个人是仅作为拓展知识了解的。

参考

vue响应式详解(重学前端-vue篇1)

0年前端的Vue响应式原理学习总结1:基本原理 - 掘金 (juejin.cn)

【尚硅谷】Vue源码解析之数据响应式原理