Vue2源码阅读-响应性系统

ObjectKaz Lv4

开始

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

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

什么是响应性

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

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

1
user.id = 1

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

  1. 通过 Proxy。这是ES6新增的一个特性,但由于偏底层,很多API在IE11等古董浏览器下无法模拟出来。但这种方式定义的响应式数据是十分完美的。后面在研究 Vue3 原理的时候我们会仔细谈到。
  2. 通过 gettersetter。这是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)
}
}

可以发现,这个函数初始化了几个重要的属性,如 datapropsmethodswatch。当然,我们先看 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 函数把对象的每一个属性转换成 gettersetter,但是 observeArray 并没有这么做,而只是把数组的每个值进行监听。数组的每一个索引并没有被设置成 gettersetter

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

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

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

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

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

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

小结:Observer 做了什么

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

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

依赖收集与变化侦测

单纯为数据定义一个gettersetter 其实并不能做什么事情。重要的是,当数据发生改变时,是如何通知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 对象。

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

  • Watcherwatcher.dirtywatcher.evaluatewatcher.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.dependchildOb 可能看起来是一个监听子对象的,暂时也忽略吧。

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]
}

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

首先看 pushTargetpopTarget。很明显,这使用了一个栈,表示把当前的 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)
}
}
}

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

这意味着,在计算属性函数调用的过程中,被访问到的所有 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 }

然后我们来看 watcherupdate 函数:

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时,lazytrue,所以,它只做了一件事情:把 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.dirtytrue,则执行 evaluate 函数,我们来看一下:

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

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

这里的逻辑就清晰了。如果 watcher.dirtytrue,那么重新执行 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 需要这个响应式数据,那么就会把自身添加到正在执行 watcherdeps(通过 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.firstNameuser.lastName单独赋值,而是整体替换。这意味着,如果只收集 user.firstNameuser.lastName 的依赖,整体替换不会触发更新。

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

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

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

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

现在我们回到/src/core/observer/index.jsdefineReactive 函数,看一下 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 对象是需要配套使用 dependnotify 的。对于对象这种情况,却并没有看到 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 这一节提到,对于数组,它的所有下标没有被定义成 gettersetter,而仅仅是对所有值做了一个 observe 操作。

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

接下来,我们来看/src/core/observer/index.jsdependArray(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.depnotify 函数。这个就和之前在 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来触发更新,和之前 defineReactivechildOb.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,以及它和 computedvm.$watch以及 watch 选项的一些关系。

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

介绍

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

  • 监听的内容:它支持函数或者表达式,最终会转换成getter
  • 通过 get 函数可以加载或刷新它的值,同时收集依赖。
  • 通过 dep.notify 调用 watcherupdate 函数,通知 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()
}

至于 pushTargetpopTarget 的相关解释,可以参考 Issue#11942

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

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

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

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 只是临时用来保存已经访问的 depid,并没有其他的作用。

取消监听

取消监听实现起来其实比较简单。只需要将它依赖的所有 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,setImmediatesetTimeout

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 进行许可。