Vue2源码阅读-响应性系统

ObjectKaz Lv4

开始

在继续阅读源码之前,可以先阅读 Vue2 官网的 深入响应式原理 一节,了解一下Vue2响应性的一些基本内容和规则。

后面,我们透过源码,可以更好的理解这些官方文档所说的内容。

什么是响应性

Vue 其中的特点,就是它的数据是响应性的,所谓响应性,说简单点,就是修改数据后,UI可以自动刷新。

例如执行下面的代码,Vue会自动触发UI的更新。

1
user.id = 1

要想实现响应性,首先这个对象一定不是普通的对象。否则,它的变化是监测不到的。要想实现响应式,首先就需要想一个办法,能够人工控制这些上面这条语句的执行。这样就可以监听变化了。解决方法有两种:

  1. 通过 Proxy。这是ES6新增的一个特性,但由于偏底层,很多API在IE11等古董浏览器下无法模拟出来。但这种方式定义的响应式数据是十分完美的。后面在研究 Vue3 原理的时候我们会仔细谈到。
  2. 通过 getter 和 setter。这是Vue2采用的办法。这种方式能够兼容古董浏览器,但是想要实现真正的响应式仍有些局限性,尤其是在数组和对象的监听上。

目标

在这一部分,我们会重点探讨 Vue 有关的几个响应式选项:

  • data
  • computed
  • watch

还有几个实例方法:

  • vm.$set
  • vm.$delete
  • vm.$watch

通过源码,理解下面图中的响应式机制:

从 data 属性开始

上面三个属性中,最简单的莫过于 data 属性了。所以我们从 data 属性开始。

寻找 data 属性的初始化位置

上一节,我们找到了 Vue 源码的入口,就是/src/core/instance/index.js ,我们先打开这个文件,然后删掉一些提示性的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

可以发现,Vue实例的众多功能大多是采用混入的方式来完成的。

Vue的构造函数也非常简单,就是调用了一个实例的 _init 方法。不过我们目前没有找到这个方法,但是下面有个 initMixin,这似乎会和这个方法有关。

接下来打开/src/core/instance/init.js 这个文件,结果一打开就发现了 initMixin 这个函数,而且里面定义了 _init 方法。

这个方法的函数体也是非常的长,不过我们现在只需要关注 data 这个属性。可是这里面找不到 data 有关的内容,不过有一个类似的词语 state,我们就先顺着 initState 函数往下找吧。

接下来打开/src/core/instance/state.js 这个文件,找到 initState 这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

可以发现,这个函数初始化了几个重要的属性,如 data、props、methods、watch。当然,我们先看 data 属性。

initData

initData 这个函数似乎看起来挺长,不过大多数仍然是一些检测性的代码,我们简化一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
proxy(vm, `_data`, key)
}
observe(data, true)
}

其中,下面几行代码应该很容易看懂:

1
2
3
4
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}

很明显,就是判断一下 data 属性是不是函数,如果是函数就执行函数调用,否则直接引用源数据。

proxy 函数

接下来, proxy 看起来很吸引人。我们来康康它的源码:

1
2
3
4
5
6
7
8
9
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}

结合这个函数的调用,就很容易理解了,这句话 proxy(vm, '_data', key) 做了一件事:就是把 vm._data[key] 代理到 vm[key] 上。这样,通过 vm.xxxx 就可以访问 vm._data.xxxx 了。

我们可以通过一个简单又具体的例子来理解这一点:

点击展开案例

可以看到,我们通过 app.message 就可以代理访问 app._data.message了。

Observer

初入 Observer

proxy函数体里面还有一个函数调用,也非常吸引人,就是 observe(data, true)。

接下来打开/src/core/observer/index.js 这个文件,找到 observe 这个函数:

这个函数仍然有一堆检测性的代码,我们简化一下:

1
2
3
4
5
6
7
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
ob = new Observer(value)
return ob
}

可以看到,简化后基本上就是 Observer 包装了一下。然后做了一个对于非对象的检测。

接下来我们看 Observer 的代码:

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

/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data

constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}

/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}

/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

可以看到,这里将 value 分为数组和对象两部分,对于对象,则直接使用 defineReactive 函数来定义响应式属性,对于数组,则做了一些特殊的处理。

Observer与对象

我们先研究value 是对象的情况,以最开始的函数调用 observe(data,true) 为例。

对于对象,它的初始化很简单,就是调用了 walk 方法。

在 walk 方法,也只是对对象的每个属性调用 defineReactive 函数。

至于 defineReactive 做了什么事情这个问题,我们暂且就认为把对象的普通属性转换成getter/setter吧。这个函数的具体细节,我们会在后面的 变化侦测 小节中提到。

但是我们可以稍微看一部分(省掉了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
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const property = Object.getOwnPropertyDescriptor(obj, key)

// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
return value
},
set: function reactiveSetter (newVal) {
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
}
})
}

可以发现,如果没有访问器,则 getter 返回了 val。而在初始化的过程中,let childOb = !shallow && observe(val) 这一行则又尝试创建一个 Observer 对象。(如果遇到非对象则直接返回)

这意味着,对于对象的响应性转换是递归的,即使是嵌套对象,也可以完整的转换成响应性对象。

Observer与数组

对于数组,可能就显得比较复杂了。所以在看源码之前,我们得回忆一下Vue中数组的响应式检测的一些要点:

  1. Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
    push、pop、shift、unshift、splice、sort、reverse

  2. Vue 不能检测以下数组的变动:

    1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
    2. 当你修改数组的长度时,例如:vm.items.length = newLength

对于第一个要点,我们就要想一个办法把原生的方法替换掉。最简单的方法莫过于直接把这几个方法添加到数组的实例方法中,从而替换数组的原生方法,然后要将这些新添加的属性的 enumerable 修饰符设置为 false。

还有一个办法,就是修改数组实例的原型,这样也避免了在数组中逐一定义实例方法,减少了循环的次数。在ES6中就有 Object.setPrototypeOf 这个函数来完成这一操作。可惜的是,当时ES6的支持度并不理想,只能使用 __proto__ 来替代。不过并不是所有浏览器都有 __proto__ 这个函数,所以考虑到兼容性就有了下面的代码:

1
2
3
4
5
if (hasProto) {
protoAugment(value, arrayMethods) // 检测到 __proto__ 就直接修改原型
} else {
copyAugment(value, arrayMethods, arrayKeys) // 检测不到 __proto__ 就逐个复制要覆盖的方法
}

我们重点研究修改原型这一方案。打开文件/src/core/observer/array.js 。

要修改原型,首先要继承原型:

1
2
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto) // 以 arrayProto 为原型创建一个对象

然后针对所有要覆盖的方法做了一个统一处理。但具体细节我们会在后面提到。

在方法覆盖完毕后,执行了对数组的侦测:

1
this.observeArray(value)

我们来仔细看一下 observeArray 函数:

1
2
3
4
5
6
7
8
9

/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}

这和 walk 函数有很大的区别。walk 函数把对象的每一个属性转换成 getter 和 setter,但是 observeArray 并没有这么做,而只是把数组的每个值进行监听。数组的每一个索引并没有被设置成 getter 和 setter。

为什么数组和对象在响应式转换上会有这样的差异呢?数组和对象的差异是,数组额外提供了很多增删数据的方法且经常被使用,这很容易导致数组位置的大规模变化。如果给每个下标添加 getter 和 setter ,那么一旦执行一些大规模增删数据的操作,那么就得不断地去调用 setter,这在性能上会有所损耗,且收效很小。

所以Vue选择修改这些操作数组的方法,而不是直接在数组元素上增加 getter 和 setter。

这样,就很容易理解下面的两句话:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue,因为数组下标不具有响应性
  2. 当你修改数组的长度时,例如:vm.items.length = newLength,同样,这也不具有响应性

关于这一方面的具体理解,可以参考这篇 Issue:

为什么vue没有提供对数组属性的监听

小结:Observer 做了什么

通过上面的一些讨论,可以发现, Observer 的主要目的是将原始对象转换成响应式的 getter 和 setter,对于不同类别的数据,他们的操作也不相同:

  • 对于对象,则会将所有的属性转换成 getter/setter
  • 对于数组,则对所有数组值调用 observe 函数。如果数组值是原始值,则没有做任何操作;如果是对象,则将其转换成 getter/setter。
graph LR
data[data选项]-->Observer(observe)-->getset[响应式访问器]

依赖收集与变化侦测

单纯为数据定义一个getter和setter 其实并不能做什么事情。重要的是,当数据发生改变时,是如何通知Vue来刷新UI和更新计算属性中的数据的。不过,目前我们暂时不研究UI的变化,所以在这一小节我们以计算属性为起点来研究这一块。

计算属性的初始化

打开文件/src/core/instance/state.js 。

我们先打开 initState 函数,可以看到初始化计算属性的代码:

1
if (opts.computed) initComputed(vm, opts.computed)

接下来找到 initComputed 函数,还是一样,先删掉一些检测性的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)

for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get

watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)

defineComputed(vm, key, userDef)
}
}

可以发现,这个函数主要做了两个工作:

  • 给每个key定义一个 Watcher。至于 Watcher 是干什么的,暂且先不讨论。
  • 每个 key 执行defineComputed 函数。

接下来找到 defineComputed 函数,还是一样,我们默认不开启SSR,只考虑传入函数的情况,我们把代码简化一下:

1
2
3
4
5
6
7
8
9
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop
Object.defineProperty(target, key, sharedPropertyDefinition)
}

最重要的,仍然是它的 getter,所以我们接着找到 createComputedGetter 这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function createComputedGetter (key) {
return function computedGetter () {
// 注:根据函数调用的逻辑,这里的 this 应该是 vm
const watcher = this._computedWatchers[key]
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}

根据函数调用的逻辑,这里的 this 应该是 vm 。

这里的 watch 就是 initComputed函数里创建的 watcher 对象,而且每个计算属性都有一个属于自己的 Watcher 对象。

所以我们需要关注几个东西:

  • Watcher 、watcher.dirty、watcher.evaluate 和 watcher.depend
  • Dep.target

再探 defineReactive

我们翻到源码的 src/core/observer/index.js,找到 defineReactive 这个函数(约第135行):

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
import Dep from './dep'
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

可能大家并不知道这个函数是干嘛的,我们先来看注释,可以大概看出这是用来在对象上定义一个响应式的数据的。

第一行代码定义了一个 dep 实例,这又是之前的 Dep 对象

中间一堆代码看上去是做一些判断,先跳过。

接下来是 Object.defineProperty,熟悉响应式原理的朋友应该清楚这是Vue2的响应式原理的核心。我们就从这里开始看吧。

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
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})

getter 那里可能看起来简单一些,大概是先调用了 getter,然后调用了 dep.depend。childOb 可能看起来是一个监听子对象的,暂时也忽略吧。

setter 这里又出现了一个 dep.notify,看来 dep 对象很重要啊。

初探 Watcher

在 createComputedGetter 这个函数里面我们看到里面一个函数返回的是 watcher.value,然后找到当前目录下的 watcher.js,发现它的构造函数里面有一句:

1
2
3
this.value = this.lazy
? undefined
: this.get()

这说明,watcher.value 这个值大概率是 get 函数的返回值。

但具体是怎么调用和刷新的,咱们先不深究。我们看到 get 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

我们还是把几行重要的代码留着:

1
2
3
4
5
6
7
8
9
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
popTarget()
this.cleanupDeps()
return value
}

可以看到它和 dep 这个库有着很强的交互。

深究 Dep

我们来找到当前目录下的 dep.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
import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;

constructor () {
this.id = uid++
this.subs = []
}

addSub (sub: Watcher) {
this.subs.push(sub)
}

removeSub (sub: Watcher) {
remove(this.subs, sub)
}

depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}

notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}

export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}

这段代码看起来很简洁,但却是这套机制的核心。

首先看 pushTarget 和 popTarget。很明显,这使用了一个栈,表示把当前的 watcher 推入和弹出 Dep.target。

再来看上面的get的代码,我们不妨可以看出其整体流程了:

graph LR
PUSH[将当前 watcher 入栈]-->CALL[调用 getter]-->POP[将当前 watcher 出栈]

这也可以看出,Dep.target 表示了当前将要执行的 getter 所在的 watcher 对象。对于计算属性来说,就是在执行计算属性定义的那个函数。在执行前入栈,执行后出栈。

接下来我们回顾 defineReactive 函数,它先创建了一个 Dep(每个 data 都有一个唯一的 Dep 对象),然后在 getter 里面调用 dep.depend 函数;在 setter 里面则调用 dep.notify。

先看 depend 函数,就是在目标 watcher 上注册自己。再来看watcher 中的 addDep 这个函数:

1
2
3
4
5
6
7
8
9
10
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

我们大概知道这个函数主要是在Dep和Watcher双方各保留一份对方的引用。

这意味着,在计算属性函数调用的过程中,被访问到的所有 data 都会在内部的 getter 执行 depend 函数,使得计算属性所在的 watcher 知道它依赖了哪些数据属性。

我们再看 notify 代码:

1
2
3
4
5
6
7
notify () {
const subs = this.subs.slice()
// ...
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}

这很明朗了,就是通知 watcher 去更新数据。而且 notify 是通过数据的 set 调用的,这意味着当 data 中的数据修改时,就会触发与之相关联的计算属性进行更新。

计算属性的缓存

在 defineComputed 函数中我们可以看到这一行:

1
2
3
4
5
6
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)

我们找到 computedWatcherOptions,发现:

1
const computedWatcherOptions = { lazy: true }

然后我们来看 watcher 的 update 函数:

1
2
3
4
5
6
7
8
9
10
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

由于定义 computed时,lazy 是 true,所以,它只做了一件事情:把 this.dirty 设置为 true

我们再来看 createComputedGetter这个函数的这段代码:

1
2
3
4
5
6
7
8
9
10
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
// 和普通的 data 是一样的
if (Dep.target) {
watcher.depend()
}
return watcher.value
}

结果发现,如果 watcher.dirty 是 true,则执行 evaluate 函数,我们来看一下:

1
2
3
4
evaluate () {
this.value = this.get()
this.dirty = false
}

这个函数就是重新执行 get 函数,然后把 dirty 设置为 false。

这里的逻辑就清晰了。如果 watcher.dirty 是 true,那么重新执行 getter,生成新的值;否则,使用原来的值。

小结

计算属性 getter 调用过程

  1. 把计算属性对应的 watcher 加入依赖目标栈中,同时设置 Dep.target
  2. 执行计算属性的 getter
  3. 执行过程中,任何被访问到的 data ,其对应的 dep 和 计算属性 watcher 都会形成响应的依赖

这整个过程称为 依赖收集。而 Dep.target 表示了被访问的 data 的访问源。

被依赖数据的更新

这个就很简单了,它会通过自己的 dep 对象通知所有的 watcher 进行更新。但只有下次访问这个计算属性的时候,才会执行计算属性里面定义的函数。

这个过程称为 依赖更新。

Dep是什么

Dep 是一个对依赖相关操作的封装。对于每一个响应式的数据(详见defineReactive),都有一个配套的 Dep 对象。

Dep.target 是一个标记,表示当前正在执行 get 操作(获取或刷新数据)的 Watcher。

有时候调用比较复杂(例如一个计算属性访问了令一个计算属性),这时候就需要使用栈来保存最近在执行 get 操作的一个 Watcher。

当 Dep.target 被设置以后,表示正在执行一个 get 操作。如果这时候有响应式数据被访问了,则说明这个 watcher 里面定义的 getter 需要这个响应式数据,那么就会把自身添加到正在执行 watcher 的 deps(通过 depend 操作,并不是立即加入,在get操作完成后会进行清理),同时也把正在执行的 watcher 添加到自身的 subs 属性。

当响应式数据发生变化时,则通知其Dep对象(notify方法),然后 Dep 对象则通知所有依赖它的Watcher,表示数据变了,需要更新。

Vue则通过这样一个巧妙的机制,实现了依赖收集和更新。

不过,由于采用变量保存当前正在执行的函数,如果这个函数含有异步代码,那么这些异步代码可能会在函数调用结束之后调用,这样,异步代码中的引用的响应式数据就不会被收集了。

数组和对象的变化侦测

上一节,我们仅仅讨论了当每个具有响应性的对象是原始值的情况,现在,我们考虑一下当这个具有响应性的对象是数组或对象的情况。

defineReactive与对象

刚刚在讨论 defineReactive 的时候,我们忽略了 childOb 这个判断。也就是说,前面我们讨论的都是当 data是一个原始值的时候的情况。

现在我们要考虑数据是数组或对象时的情况了。

我们先用一个简单的例子来理解一下对象的变化检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new Vue({
data: {
user: {
id: 1,
age: 19,
firstName: "Foo",
lastName: "Bar"
}
},
computed: {
fullName(){
return this.user.firstName + this.user.lastName
}
}
})

那什么时候需要刷新 fullName 呢?一共有三种情况:

  1. user.firstName 发生了变化
  2. user.lastName 发生了变化
  3. user 发生了变化

这也意味着需要收集上面三个响应式数据对应的依赖。

其中,第三种情况可能不太容易想到。一旦user发生了变化,例如赋了一个新的值:

1
2
3
4
5
6
this.user = {
id: 2,
age: 19,
firstName: "Dream",
lastName: "George"
}

但这毕竟不是对user.firstName 和 user.lastName单独赋值,而是整体替换。这意味着,如果只收集 user.firstName和 user.lastName 的依赖,整体替换不会触发更新。

从中我们得到一个结论:对于响应式数据是对象的情况,在收集依赖时,不仅要收集所访问属性的依赖,也要收集对象本身的依赖。

好在,实现这个结论是非常容易的,因为要想访问 user.firstName,就必须先访问 user。这样就可以在这个过程中收集这方面依赖。

不过这又出现了一个问题,就是在上面的计算属性中,user实际上被访问了两次,这意味着同一个 Dep 可能会被添加两次。这是没有必要的,因为 dep 仅仅需要表示是否有依赖关系,但是依赖了几次是不需要管的。所以,尽管访问了两次,但只需要添加一次依赖。

所以,在
/src/core/observer/watcher.js 中的 get 方法中,依赖收集完毕后,会执行 cleanupDeps() 进行清理,把重复的依赖清理掉。

现在我们回到/src/core/observer/index.js 的 defineReactive 函数,看一下 childOb 相关的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义处
let childOb = !shallow && observe(val)

// getter
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}

// setter
childOb = !shallow && observe(newVal)

对于 setter,如果整体被赋了新的值,那么这个新的值会自动被注册成响应式的。

不过,childOb.dep.depend() 这句可能并不好懂,但对于我们目前的认知,Dep 对象是需要配套使用 depend 和 notify 的。对于对象这种情况,却并没有看到 childOb.dep.notify 这句话。

我们暂且就把它当成为了健全性做的一些预留操作吧。

defineReactive与数组

讨论完对象,我们接着讨论数组。

和对象一样,我们先来看一个例子,来理解一下相应的过程。

假设现在需要通过数组来动态增加或删除一个TODO-LIST,为了保证代码的简洁,我们允许数组元素是字符串或者是对象:

1
2
3
4
5
6
7
8
9
10
11
12
new Vue({
data: {
list: [
"吃饭",
"睡觉",
{
title: "打豆豆",
status: "DONE"
}
]
}
})

在 Observer 这一节提到,对于数组,它的所有下标没有被定义成 getter 和 setter,而仅仅是对所有值做了一个 observe 操作。

这意味着,要想收集数组的依赖,就只能通过数组本身的 __ob__.dep 属性,而无法通过数组元素的访问。这也是 childOb.dep.depend() 对于数组的含义。

接下来,我们来看/src/core/observer/index.js 的 dependArray(value) 函数:

1
2
3
4
5
6
7
8
9
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}

这个函数也是十分简单,对于每个元素的操作和下面的代码几乎是一样的:

1
2
3
4
5
6
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}

接下来我们来研究一下被修改的几个数组方法:/src/core/observer/array.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
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result
})
})

我们发现,在调用的末尾处,调用了数组所在 observer.dep 的 notify 函数。这个就和之前在 defineReactive 中,为对象的 childOb 调用的 depend 函数相对应。

除此之外,如果数组新增了元素,那么这个函数会收集新增的元素,因为这些元素可能没有被转换成响应性。而删除的元素则无需处理。

vm.$set

当通过常规方法无法使数组或对象的更新被检测到,那么就只能通过 vm.$set 或者 vm.$delete 来进行操作了。

首先,我们还是得找到 vm.$set 的定义,通过全局搜索$set,我们找到了vm.$set 的定义位置在/src/core/instance/state.js

然后找到了 set 函数的位置在/src/core/observer/index.js 。不过还是先删掉一些检测性的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function set (target: Array<any> | Object, key: any, val: any): any {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}

先看对于数组的操作:

1
2
3
4
5
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}

这段代码其实比较好理解,因为它本质还是调用了数组的splice 方法,先删除一个元素,再插入一个元素。

接下来,则是对已有属性的操作:

1
2
3
4
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}

因为这种情况下的操作本身就具有响应性,所以直接赋值就行了。

最后来看新增属性的操作:

1
2
3
4
5
6
7
8
const ob = target.__ob__
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val

如果没有找到 __ob__ 属性,那么这就是一个普通对象,而不是具有一个响应性的对象,那么直接赋值就行。

如果找到了 __ob__,那么就是在响应性对象上新增一个属性,则调用 defineReactive 函数来将属性转换成具有响应性的对象。而且这里使用了 ob.dep.notify来触发更新,和之前 defineReactive 的 childOb.depend 相对应。

可见,之前在 defineReactive里面,除了把自身加入依赖中,还把childOb.dep 加入依赖,主要是两个目的:

  • 方便数组被覆盖的一些操作的更新触发
  • 方便通过 vm.$set 新增对象属性时的更新触发

vm.$delete

寻找 vm.$delete 的方法不再赘述。它的源码在/src/core/observer/index.js 。

对于 vm.$delete 操作,我们也简化一下源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function del (target: Array<any> | Object, key: any) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = target.__ob__
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}

对于数组的处理,它和 vm.$set 方法一样,是通过被覆盖的数组方法来实现的:

1
2
3
4
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}

接下来是一个判断 hasOwn(target, key)。这用来判断对象上的某个属性是来自自身还是来自它的原型。对于来自原型的属性,delete 操作不会作任何事。

1
2
3
if (!hasOwn(target, key)) {
return
}

这里的判断是有必要的,因为要避免不必要的更新被触发。

然后判断是否有 __ob__ 属性。对于不具有响应性的对象,没有必要触发更新。

1
2
3
if (!ob) {
return
}

最后则是通过__ob__的 dep 来触发更新。

Watcher

这一节,我们将深入探讨 Watcher,以及它和 computed、vm.$watch以及 watch 选项的一些关系。

Watcher源码:/src/core/observer/watcher.js

介绍

根据之前的一些用法,我们可以看出,Watcher 本质就是一个侦听器对象。

  • 监听的内容:它支持函数或者表达式,最终会转换成getter。
  • 通过 get 函数可以加载或刷新它的值,同时收集依赖。
  • 通过 dep.notify 调用 watcher 的 update 函数,通知 watcher 刷新数据并执行回调。
  • 通过 value 属性可以拿到缓存的值。

watch 选项与 Watcher

还是一样,我们先打开/src/core/instance/state.js 中的 initState 函数,可以发现初始化 watch 选项的代码:

1
initWatch(vm, opts.watch)

接下来找到 initWatch 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}

这里为了简便,我们假设 watch[key] 不是数组。

接下来我们找到 createWatcher(vm, key, handler) 这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}

可以发现,watch 属性仅仅只是对 vm.$watch 的一个封装而已。

vm.$watch 和 Watcher

先来回顾一下 vm.$watch 的用法:

vm.$watch( expOrFn, callback, [options] )

参数:

  • {string | Function} expOrFn
  • {Function | Object} callback
  • {Object} [options]
    • {boolean} deep
    • {boolean} immediate

返回值:{Function} unwatch

用法:

观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 带监听选项
$vm.watch('test',{
handler: (newValue) => console.log(newValue),
deep: true,
immediate: true
})

// 监听一个复杂的表达式
$vm.watch(() => this.data[this.id],(newValue) => console.log(newValue))

// 取消监听
let unwatch = $vm.watch('test.a.b',(newValue) => console.log(newValue),{
deep: true,
immediate: true
})

// some place
unwatch()

我们先打开/src/core/instance/state.js 中的 stateMixin 函数,可以发现初始化 watch 操作的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}

首先判断回调是否是纯对象(调用toString函数结果是 [object Object]):

1
2
3
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}

如果是纯对象,则跑回去调用 createWatcher 函数,但之前也看到了,createWatcher 函数只是针对参数做一些适配性的处理,然后又调用 vm.$watch。

可以看到,接下来它创建了一个 Watcher 对象:

1
new Watcher(vm, expOrFn, cb, options)

接下来,如果设置了 immediate选项,则立即调用一次回调函数:

1
2
3
4
5
6
7
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
// 相当于 handler.apply(vm, [watcher.value]) ,只是在这个基础上加上了错误处理
popTarget()
}

至于 pushTarget 和 popTarget 的相关解释,可以参考 Issue#11942 。

接下来返回了一个停止监听的函数:

1
2
3
return function unwatchFn () {
watcher.teardown()
}

这个函数仅仅是简单的调用了一个watcher的teardown 函数。这个函数的细节我们会在后面提到。

lazy 模式与计算属性

lazy 模式是专门针对计算属性的,默认情况下,所有计算属性的 Watcher 全部开启了 lazy 模式。

watcher 内部有一个标记变量 lazy,表示当前是否开启lazy模式;同时有一个 dirty 属性,表示当前数据是否过期。

开启 lazy 模式后,value 属性默认为undefined:

1
2
3
4
// constructor
this.value = this.lazy
? undefined
: this.get()

如果需要刷新值,则需要运行evaluate ()方法:

1
2
3
// evaluate
this.value = this.get()
this.dirty = false

但是 evaluate 方法不会检测 dirty,所以一旦执行就会刷新数据,因此在执行 evaluate 之前,需要判断 dirty 属性:

1
2
3
if (watcher.dirty) {
watcher.evaluate();
}

当 update 函数被调用时,数据不会被立即刷新,而是在下次访问evaluate方法的时候刷新:

1
2
// update 函数
this.dirty = true

不过,对于 lazy 模式的watcher,回调函数似乎并没有什么卵用,也不会执行。

深度监听

watcher 使用一个 deep 属性来确定当前是否是深度监听:

1
2
3
4
// get()
if (this.deep) {
traverse(value)
}

如果开启了深度监听,则执行了 traverse 函数/src/core/observer/traverse.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
const seenObjects = new Set()

/**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*/
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
// 绕过原始值
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
// 是否是响应式对象
if (val.__ob__) {
const depId = val.__ob__.dep.id

// 避免重复访问,同时也阻止了循环引用导致的死循环
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen) // 核心!
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen) // 核心!
}
}

这段代码看起来很复杂,实际上核心就是访问一遍整个对象的所有属性 val[i] 或者 val[keys[i]]。在访问属性的过程中,会将属性对应的依赖收集到这个 watcher 的依赖列表里。

至于 seenObjects 只是临时用来保存已经访问的 dep 的 id,并没有其他的作用。

取消监听

取消监听实现起来其实比较简单。只需要将它依赖的所有 Dep 解除即可。

首先,watcher 使用了 active 属性来保存当前的 watcher 是否可用:

1
2
// constructor
this.active = true

接下来当 teardown 方法被执行时,则一个个将依赖移除,然后将标志变量this.active 设置为 false。

1
2
3
4
5
6
7
8
9
teardown () {
if (this.active) {
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}

更新调度

对于非 lazy 模式的 watcher,在 update 函数执行后,会判断它的更新方式是否是同步(默认情况下this.sync = false):

1
2
3
4
5
if (this.sync) {
this.run()
} else {
queueWatcher(this)
}

可以发现,如果是同步更新,则直接调用 this.run 触发更新的处理,如果是异步,则将其加入队列中。异步调度的具体内容将在后面的小节中提到。

接下来我们可以康康 run 函数做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
// 相当于 this.cb.call(this.vm, [value, oldValue])
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

首先获取了新的值:

1
const value = this.get()

接下来有一个判断值得注意:

1
2
3
value !== this.value ||
isObject(value) ||
this.deep

很明显,这个判断是用来确定是否更新值的。但这个判断不太容易懂,我们来分析一下:

  • 对于原始值来说,如果值没有发生变化,就不需要执行后面的更新操作。

  • 对于对象,即使值没有变,如果某个属性变了,那么仍然属于发生了变更。

    只有深度监听的情况才可以监听对象属性的变化。
    但也有可能出现下面这种先取值,修改数据后重新赋值的情况:

    1
    2
    3
    let obj = vm.a
    obj.b = 2333
    vm.a = obj
  • 对于深度监听的对象,应该执行更新操作。这应该是出于完备性的一个考虑。

最后则是调用了回调函数:

1
this.cb.call(this.vm, value, oldValue)

异步更新队列

在 Vue官网的响应式原理中,提到了 异步更新队列 的有关知识,在这里,我们会对Vue的异步更新队列作进一步的探讨。

这一块的内容需要你提前掌握 javascript 事件循环的有关知识,如果没有掌握,请先去了解这些知识。

nextTick

介绍

对于大多数人来说,这个函数是非常实用的,它可以让你在DOM更新完成后做一些事情。

一个典型的例子,便是刷新路由视图:

1
<router-view v-if="showView"></router-view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
data() {
return {
showView: true
}
},
methods: {
refresh() {
this.showView = false
this.$nextTick(() => {
this.showView = true
})
}
}
}

这样,当执行 this.showView = false 时,路由视图就会被销毁,因为在DOM更新结束前, this.showView = true 还没有被执行。

当路由视图被销毁以后,this.showView = true 便会被执行,这时候会再次加载DOM,从而实现了router-view组件的刷新。

但如果只是这么写:

1
2
this.showView = false
this.showView = true

就不会起到效果,因为DOM的更新是异步集中更新,多个更新最终会被合成为一个。上面的代码也相当于:

1
this.showView = true

再例如,如果需要对正在更新的DOM进行一些DOM操作,则最好通过 vm.$nextTick,因为DOM更新触发之前,你拿到的是更新前的DOM数据。

关于虚拟DOM及其更新的一些解读,请看后面的文章。

nextTick 的源码在 /src/core/util/next-tick.js ,整段代码只有100行左右:

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
/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

清空队列

我们先从 flushCallbacks 入手:

1
2
3
4
5
6
7
8
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

这个函数的执行逻辑非常简单,就是把数组中所有的回调函数执行一遍,然后清空回调数组。

nextTick 的实现

在了解它的实现之前,可以先看看它的用法。nextTick文档

这里对它的用法作一个简单的描述:

  • 如果传入了回调函数,则会执行回调
  • 如果没有传入回调,则会返回一个 Promise,待DOM刷新完毕后,执行 .then 回调

理解了它的用法,再去看它的源码便很简单了。

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
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
// 有回调,执行回调
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
// 没有回调,执行 promise 的 _resolve 回调
} else if (_resolve) {
_resolve(ctx)
}
})

// 当前是否已经设定了清理队列的定时函数
// 如果没有设置,则设置
// 这样一次定时函数调用,可以执行多个 nextTick 函数
if (!pending) {
pending = true
timerFunc()
}

// 如果没有回调函数,则返回 Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

定时函数

nextTick 函数的源码中,有相当大一部分代码是进行 timerFunc 的设置。而且在不同的版本中,timerFunc 的设置方法也一直在变化。

第一版中 timerFunc 的执行顺序为 Promise, MutationObserver, setTimeout。[1]

在2.5.0这一版中,timerFunc 的顺序被改为 setImmediate, MessageChannel, setTimeout。在这一版,所有的微任务都被取消了,因为微任务的优先级太高了,导致了像 #6566 这样的问题。[1:1]

但是,仅使用宏任务又出现了像 #6813 这样微妙的重绘问题。

所以,在2.6+版本,又改回了微任务,并采用了一些特殊手段来解决 #6566 这样的问题。

目前这个版本的顺序是:Promise, MutationObserver,setImmediate,setTimeout

Watcher 更新调度

queueWatcher

在 更新调度 这一小节里提到,如果更新是异步的,那么便会执行下面的函数:/src/core/observer/scheduler.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
export function queueWatcher (watcher: Watcher) {
const id = watcher.id

// 防止重复入队
if (has[id] == null) {
has[id] = true

// 如果没有在清理 watcher,那么则直接推入
// 否则按照 watcher.id 的顺序,将当前 watcher 插入
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}

// 是否已经设置了刷新等待
// 如果没有设置,则设置
if (!waiting) {
waiting = true

if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}

这里有几个要点值得注意一下:

  • 每个 watcher 只会被放入一次,这意味着当某个 watcher 收到多个改变时最终只会被执行一次。
  • 刷新 watcher 时,watcher 会按照次序放入队列,否则直接加到末尾。
  • 刷新后的第一次调用会开启刷新等待。

flushSchedulerQueue

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
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id

// 排序
queue.sort((a, b) => a.id - b.id)

// 逐个执行队列函数
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}

// 重置前复制一份
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()


// 状态恢复,把一些标志变量恢复到原始值
resetSchedulerState()

// 更新完毕后调用钩子
callActivatedHooks(activatedQueue) // 根据它的注释,这个是和 keep-alive 有关的,暂时不考虑
callUpdatedHooks(updatedQueue) // 调用 updated 钩子
}

// 如果队列 watcher 是 render watcher,那么调用 updated 生命周期钩子
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}

这里有一个值得注意的地方:执行函数之前,需要对 watcher 进行排序。

从注释上看,有三点原因:

  1. 组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。[2]

  2. 用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。[2:1]

  3. 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。[2:2]

原理图


  1. Vue源码——nextTick实现原理, 红尘炼心, 掘金 ↩︎ ↩︎

  2. Vue技术揭秘 ↩︎ ↩︎ ↩︎

  • 标题: Vue2源码阅读-响应性系统
  • 作者: ObjectKaz
  • 创建于: 2021-09-26 15:08:49
  • 更新于: 2022-03-15 13:53:31
  • 链接: https://www.objectkaz.cn/731b8d08837c.html
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。