Vue2.x-数据响应Watcher篇详解

前言

上一章已经简单介绍了Vue2.x的数据响应系统的三个重要角色Observer、Dep、Watcher
本篇主要想讲述用户每次操作或者数据变动到底会发生什么?换个说法Watcher是如何收到通知去进行更新操作的?

Watcher

上一章也提到了Watcher的三种类型

  1. computed-watcher
  2. normal-watcher
  3. render-watcher

computed-watcher

在组件初始化的过程中,Vue会为每个计算属性都会生成一个Watcher,并且存到当前组件实例的_watchers里,但是这类watchers不会被立即和数据的Dep进行关联,这类watcher声明时使用了lazy属性
意味着在第一次调用的时候才会去添加依赖 初始化代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function initState (vm) {
vm._watchers = [];
var 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);
}
}

重点看initComputed

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
var computedWatcherOptions = { lazy: true };
function initComputed (vm, computed) {
// $flow-disable-line
var watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
var isSSR = isServerRendering();

for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}

if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}

// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
}
}
}
}

以上代码会遍历computed内的键值对,为每个计算属性声明一个Watcher并将该watcher以键值对的形式存在vm.computedWatchers里,
注意lazy传的true,所以在执行构造函数的时候不会执行watcher.get,不会将该watcher收集到经过Observe劫持的数据的__ob
_.dep.subs里
再看defineComputed这个方法

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
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function defineComputed (
target,
key,
userDef
) {
var shouldCache = !isServerRendering();
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
("Computed property \"" + key + "\" was assigned to but it has no setter."),
this
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}

defineComputed主要目的是在组件实例上定义该计算书型的getter和setter,还区分了用户有没有手写set/get,
在页面调用这个计算属性的时候会调用createComputedGetter,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}

这里我们关注watcher.evaluate(),evaluate内部会重新执行watcher.get()进行依赖收集,
上一章提到过,并且返回最终计算结果

normal-watcher

这类watcher是指watch钩子里声明的观察函数,当然也可以通过在组件mounted手动通过this.$watch()手动去声明,后面我们会讲述这两者的区别
看initWatch代码:

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

initWatch会遍历每个watch钩子里的键值对,创建一个Watcher实例,从以上代码可以分析出watcher可以有三种声明方式
createWatcher如下:

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

而vm.$watch执行的就是new Watcher的操作了,此类型的watcher实例在执行构造函数的时候就会去调用watcher.get(),这点和computed-watcher不一样哦~

render-watcher

上一章讲到过,这类watcher只会在每个组件$mount时声明,意味着每个组件实例都会有一个此类型的watcher,代码就不展示了,
这类watcher的作用就是提供组件内部更新的回调函数

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<div id="root">
<span>{{ name }}</span>
<span>{{ name }}</span>
<span>{{ age }}</span>
<button @click="handleAddAge">add</button>
</div>
<script>
new Vue({
el: '#root',
data() {
return {
name: 'demo',
age: 12,
}
},
computed: {
res() {
return this.name + this.age
},
},
watch: {
age() {
this.name += 1
},
},
methods: {
handleAddAge() {
this.age += 1
},
},
})
</script>

上面是一个简单的demo,以此demo我们分析一下初始化过程的依赖收集和用户点击按钮会发生什么吧hah

初始化

  1. initComputed
  2. initWatch

当然这两个步骤都是发生在Vue劫持了data、props之后的
我们打印出组件实例 依赖收集的情况如下图所示:
依赖

  1. _computedWatchers存放就是每个计算属性的观察者实例
  2. _watcher存放的是当前组件初始收集依赖和更新所以要的观察者
  3. _watchers存放的是组件声明所有的watchers队列

示例图最下面,Vue会为data里的每个值声明getter和setter,在使用defineReactive定义的时候,会创建Dep实例

  1. 所以初始化的结束后,name创建的Dep实例的subs里会存放一个watcher,为render-watcher,用于更新组件
  2. 而age也声明在watch里,所以它对应的Dep实例subs会存放两个watcher,一个normal-watcher和一个render-watcher
  3. 而在res被使用后,age和name创建的Dep实例的subs都会多一个computed-watcher

数据变化

点击add按钮会让age实现自增,相应会触发watch,同时更新视图,这是给开发者看到的情况,那更往下呢,我们慢慢分析

执行了this.age += 1会触发age的setter 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function reactiveSetter (newVal) {
var 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 (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();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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()
}
}

触发dep.notify从而触发watcher.update
注意是先排序再触发,id是自增的,说明watcher触发的顺序是一定的,也就是声明顺序的computed-watcher > normal-watcher > render-watcher

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

queueWatcher

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
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
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)
}
// queue the flush
if (!waiting) {
waiting = true

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

代码里有个has集合,用于存放本次更新触发的watcher,避免了同一个watcher被触发两次,也算是节省了性能
nextTick大家应该都不陌生,这个api作用实际上就是把更新任务方法放到微任务去做,这样做的好处我们用代码说明吧
demo里触发了按钮的回调,修改了this.age正常来说,this.age修改后就应该调用render-watcher的更新回调来更新视图,然后触发watch钩子里的age监听修改name,重复操作再次更新视图
这样就导致了可能一次操作要重新调好几次update,而且更新视图是比较消耗性能的,其间包括了重新生成vnode,与oldVnode进行diff比较,updateChildren等一系列操作。

如何让一个一次改动所带来watcher的执行共用一次更新操作呢?

nextTick起到了重大的作用,拿demo来讲,在age变化触发normal-watcher,queueWatcher方法后把这个任务方法放到微任务里,这样这个微任务不会立马执行,
主线程会继续执行,会触发age对应的render-watcher,但是此时waiting为true,render-watcher不会触发flushSchedulerQueue,但是此时render-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;
var watcher, id;

// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort(function (a, b) { return a.id - b.id; });

// do not cache length because more watchers might be pushed
// as we run existing watchers
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 (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
}
}
}

// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();

resetSchedulerState();

// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);

// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush');
}
}

这一步就是直接遍历queue,触发watcher.run去执行watcher的回调函数
我们先看看watcher.run的代码

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
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

这部分代码目的就是执行watcher的回调
对于render-watcher来说,this.get()执行的是组件的更新方法
对于normal-watcher来说,this.get()执行的是watcher的handler

按上述demo的执行顺序

  1. 执行age对应的normal-watcher,执行了this.name += 1,此时又会触发name的setter,而name对应的Dep实例里只有render-watcher
  2. 但是此时age变化导致的render-watcher还没有触发,has集合里毅依然存在组件更新的回调,所以age对应的render-watcher不会被加到queue队列里
  3. 最后才会执行render-watcher一顿更新操作
  4. queue里的watcher都执行完毕会重置has、waiting等属性,表示次轮更新结束

讲到这儿,可能还是有点懵逼吧,看图
queueWatcher