前言
在响应式系统源码分析前,我们先来了解几个基本概念,顺便弄清楚 Vue 响应式与变化侦测的核心原理。这将有助于我们后面更好的理解源码。
响应式与变化侦测
所谓响应式就是数据和视图之间的双向绑定关系,当数据变化时视图会自动更新,视图变化时数据也会自动更新。
变化侦测就是当某个数据发生变化时,需要侦测到是哪个数据发生变化了,然后执行一系列操作(如通知对应的视图更新)
如何追踪变化
我们已经知道为什么需要变化侦测了,就是在数据发生变化时要侦测到,然后执行一系列操作。那么如何侦测变化呢?
在 JavaScript 中有 Object.defineProperty 和 ES6 的 Proxy 两种方式实现数据变化的侦测,前者是 Vue2.0 所采用的方式,因为在那时候浏览器对 ES6 的支持度普遍并不高,所以没有采用 Proxy。
那 Vue2.0 使用 Object.defineProperty 是怎么实现数据侦测的呢?其实很简单,主要在 Object.defineProperty 的第三个参数属性描述符中运用 get 和 set 函数来对属性进行拦截。来看个例子:
js
<script>
export default {
data() {
return {
name: 'hello world'
}
},
mounted() {
console.log(this.name) // hello world
}
}
</script>
定义在 data 中的 name 属性,为什么我们能直接通过 this.name 访问到,它不是放在 data 对象中吗?按理说应该要通过 this.data.name 才能访问到,那为什么能省去 data 直接通过 this 访问到呢?这就是属性描述符里 get 函数的作用,如下:
js
Object.defineProperty('vm', 'name', {
get() {
return vm['_data']['name'],
},
set(value) {
vm['_data']['name'] = value
}
})
这段代码不完全是 Vue 源码中的,但是目的相同,这里的 this 就是 vm(组件实例),第一个参数是 vm (即定义属性的对象)。第二个参数是要定义或修改的键,这里是 name。第三个参数是属性描述符,一般会设置 get / set 函数,这意味着当我们通过 vm(目标对象)去访问定义的这个属性(name)时,会触发属性描述符中的 get 函数,同理修改这个键的取值的时候会触发 set 函数。
代入到上边我们通过 this.name(等价于 vm.name)访问时,会触发 get 函数,而 get 函数内部就已经帮你访问 vm 上的 _data(这里的 _data 就是我们在组件里写的 data 对象,名称不一样而已),所以我们就能省去访问 data 这一步。这也是为什么我们能直接通过 this 来访问 Vue 上的各种属性和方法。
既然目标对象上属性的读取/修改都能侦测到,那你可以以实现响应式为目标去想想在 get / set 函数中可以做些什么呢?
如何收集依赖和通知依赖?
通过上边例子我们可以很容易想到可以在 get 函数中收集依赖。此处我们为了方便理解,我们把负责收集依赖的称为发布者,被收集的依赖称为订阅者。
那么所谓收集依赖其实就是收集订阅者。为什么要在 get 函数中收集依赖呢?因为 get 函数的触发意味着有人(订阅者)访问了这个数据,如果我不进行收集,假设这个数据变化了,那我该通知谁去更新视图呢?很显然我需要在访问这个数据的时候就把访问我的这个人(订阅者)收集起来,以便之后数据发生变更通知它(订阅者)。
一个发布者可以对应多个订阅者,即可以有多个订阅者同时订阅某个数据,当数据变更时,发布者要通知所有订阅者。
那么在 Vue 里,有一个专门表示发布者的类,叫 Dep,通过 new Dep() 的方式可以创建一个 Dep 实例(即创建一个发布者),这个 Dep 实例(发布者)中维护着一个 subs 列表(订阅者列表)。下面贴出这个 Dep 类的源代码,此处仅需明白流程就行,具体源码会在后面文章中分析。

在 Vue 里每个数据都会对应一个 Dep 实例,当有依赖(订阅者)访问这个数据时,会触发 get 函数,get 函数中通过 depend 函数收集访问这个数据的依赖(订阅者),在这个数据发生变更触发 set 函数时,会调用 notify 函数来通知所有依赖(订阅者)。大致流程如下图所示:

依赖是谁?
搞清楚了 Vue 如何收集依赖及通知依赖触发更新,那我们现在关心的就是收集的这个依赖到底是谁。换句话说,就是当属性发生变化后,通知谁。
要通知用到数据的地方,而用到数据的地方有很多,即有可能在模板中使用,也可能是用户的一个 computed,还可能是用户写的一个 watch,比如下边几种情况:
- 模板中使用 name 属性:
vue
<template>
<div>{{ name }}</div>
</template>
- computed 中使用到 name 属性:
js
<script>
export default {
data() {
return {
name: 'hello'
}
},
computed: {
fullName() {
return this.name + ' world'
}
}
}
</script>
- 用户自定义 watch 监视了 name 属性:
js
<script>
export default {
data() {
return {
name: 'hello'
}
},
watch: {
name: {
handler(val) {
console.log(val);
}
}
}
}
</script>
上边是依赖 name 属性三种情况,这里就需要抽象一个能集中处理这些情况的类,在 Vue 中对应的就是 Watcher 类了。Watcher 是作为一个中介角色,数据发生变化时通知它,然后它再去通知其他地方。所以收集依赖其实就是收集各种 Watcher,Watcher 有很多种,比如模板中的 Watcher、用户自定义 Watcher、computed Watcher。
总结
本篇我们了解了响应式和变化侦测的基本概念,以及 Vue2.0 中使用 Object.defineProperty 实现了变化侦测,依赖收集和通知依赖更新的时机,最后知道收集的依赖其实就是各种 Watcher,Watcher 有很多种,比如模板 watcher、computed watcher、用户自定义 watcher。
理解了本章的内容有助于我们更高效、更深入的去看 Vue 的源码。从下篇开始,我们就正式开启 Vue 响应式系统源码的分析。
本文对你有帮助的话,点个赞支持下哈~