Vue源码探秘(派发更新)
Cosen95 opened this issue · comments
引言
在上一节,我们分析了响应式数据中依赖收集
的过程,而收集依赖的目的就是在数据更新时会遍历订阅者并派发更新。这一节,让我们一起来看下派发更新
的过程。
setter
当数据被修改时会触发setter
,回顾setter
函数:
// src/core/observer/index.js
/**
* 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() {
// ...
},
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();
},
});
}
这里setter
做了两件事情:修改属性值和派发更新。
首先,setter
会通过getter
拿到修改前的值value
和修改后的值newVal
做比较,如果两者相同直接return
(表示值没有发生变化)。
这里的
newVal !== newVal && value !== value
表示的是value
和newVal
都是NaN
,因为NaN
不等于自身。
接着调用了customSetter
函数。
再接下来的这部分就是关键逻辑了:首先修改属性值,如果之前定义了setter
则直接调用setter
,如果没有则做一个赋值操作。
接下来的这句:
childOb = !shallow && observe(newVal);
其实在调用defineReactive
函数时,他已经执行一遍了,这里再次执行是考虑到数据可能原来没有被观察,而现在被修改为数组或者纯对象,那就需要调用 observe
将它转换成为一个响应式对象。
最后的dep.notify()
就是派发更新了,也正是本节要分析的重点。
派发更新
先来看 notify
函数的定义:
// src/core/observer/dep.js
export default class Dep {
//...
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();
}
}
}
这里首先拿到了subs
的副本。
这里采用了
Array.slice
实现了数组的拷贝,其实还有很多方法可以实现。具体参考这里
之后的if
判断中的config.async
是一个全局配置,默认值为true
,用来表示观察者是异步执行还是同步执行。
如果是同步执行,则需要在这里给 subs
中的 Watcher
按 id
从小到大的顺序进行排序。这里为什么要有一个排序操作呢?先留个疑问,我们接着往下看。
接下来就是遍历 subs
中的 Watcher
并执行它们的 update
方法。我们来看 update
方法的定义:
// /src/core/observer/watcher.js
export default class Watcher {
//...
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
// ...
}
这里的三条条件语句分别对应Watcher
的不同状态。前两种分别对应计算属性
、同步Watcher
,会在后面详细介绍。当前会走else
逻辑执行queueWatcher
函数。queueWatcher
函数定义在src/core/observer/scheduler.js
中:
// src/core/observer/scheduler.js
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
const queue: Array<Watcher> = [];
let has: { [key: number]: ?true } = {};
let waiting = false;
let flushing = false;
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);
}
}
}
queueWatcher
函数会把传入的 Watcher
推入到一个异步的观察者队列 queue
中。
为什么要引入队列呢,实际上这是一个优化手段。它并不会每次数据更新时,就直接遍历
subs
执行Watcher
的回调,而是将这些Watcher
放入一个异步队列,调用nextTick
来异步执行flushSchedulerQueue
。
函数首先定义了一个对象 has
用于记录传入 Watcher
的 id
,它的作用是避免同一个 Watcher
被重复推入队列。
这种情况什么时候会出现呢,比如我们同时修改了被同一个
Watcher
订阅的多个数据,那么会触发多个setter
,也就会调用queueWatcher
函数多次。
这里定义 flush
为 false
,所以会走 if
逻辑将 Watcher
push 到 queue
中。flush
的作用以及 else
的具体逻辑现在还不清楚,需要我们往下分析才知道。
最后的 if
语句中的 waiting
和上面的类似,也是为了保证 if
里面的逻辑只执行一次。这里再次遇到之前的同步异步的判断,如果是同步则直接执行 flushSchedulerQueue
函数,否则执行 nextTick(flushSchedulerQueue)
。
nextTick
方法的具体实现会在下一节详细介绍。接下来我们就来看 flushSchedulerQueue
函数的定义:
// src/core/observer/scheduler.js
function flushSchedulerQueue() {
currentFlushTimestamp = getNow();
flushing = true;
let 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((a, b) => 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++) {
// ...
}
// ...
}
flushSchedulerQueue
函数一开始会将 flushing
置为 true
,然后对 queue
做一次按 id
从小到大的排列(也就是按从父到子的顺序排序)。注释说明了做这个排序的原因:
1、组件的更新顺序是从父到子,因为组件的创建顺序是从父到子。
2、组件的 user Watcher
会优先于渲染 Watcher
执行。(user Watcher
什么情况下出现呢,比如我们在组件对象中使用 watch
属性或者调用 $watch
方法时就会创建一个 user Watcher
)。
3、如果一个组件在它的父组件的 Watcher
执行的时候被销毁,那这个组件对应的 Watcher
的执行可以被跳过。
回顾前面的 dep.notify()
,我们在那里提出了一个问题:为什么同步执行的 Watcher
要在 dep.notify()
就先排好顺序?
现在可以解开疑惑了:由于异步 Watcher
的 flushSchedulerQueue
函数会在所有 Watcher
入列后才执行,所以可以正确排序,而同步 Watcher
的 flushSchedulerQueue
函数是同步执行的,不会等所有 Watcher
入列后才执行,所以无法保证 queue
能正确排序,所以干脆在 dep
中就先把 subs
排好。
在 queue
排好序之后就要遍历 queue
中的 Watcher
。注意在 for
循环前的一段注释:
// do not cache length because more watchers might be pushed
// as we run existing watchers
注释的意思是不要缓存length
(也就是不要在for
循环前先获取queue.length
)。这么做的原因是什么呢?
因为在执行 Watcher.run()
时可能有新的 Watcher
加入进来导致 queue
长度发生变化。而新加入的 Watcher
就又会执行到 queueWatcher
函数:
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);
}
// ...
}
}
这个时候 flushing
已经被修改为 true
了,所以会执行 else
逻辑。
其实在上面的分析中,我们只看了if
逻辑:也就是一开始Watcher
入队时。
当所有 Watcher
入队后会遍历执行回调。而在执行 Watcher
过程中可能会有新 Watcher
入队,这个时机就对应 else
逻辑。
else
中不能简单地将 Watcher
push 到 queue
中,因为 queue
在遍历执行前已经排好了顺序,这里必须保证新插入的 Watcher
不会打乱顺序,所以需要计算出正确的插入位置。while
的判断逻辑是这样的:
i > index && queue[i].id > watcher.id;
i
取值queue.length - 1
,也就是queue
的最后一位。所以 i > index
的意思是在 [index, i]
的区间从后往前找,然后一直找到 queue[i].id
小于(不可能等于) watcher.id
的位置,找到后将 Watcher
插入。
看完这块,我们回到flushSchedulerQueue
函数,接着往下看下for
循环的代码:
// src/core/observer/scheduler.js
// 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 (process.env.NODE_ENV !== "production" && has[id] != null) {
// ...
}
}
这里的逻辑是如果 Watcher
定义了 before
就先执行 before
,接着将这个 Watcher
在 has
中对应的值置为null
,然后调用 watcher.run
。我们来看 watcher.run
的定义:
// src/core/observer/watcher.js
export default class Watcher {
// ...
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);
}
}
}
}
// ...
}
这里调用 this.get()
方法拿到 value
,在上一节我们有介绍过 get
函数,它会手动调用数据的 getter
拿到属性值返回。接着如果满足以下任一条件,就执行回调 this.cb
,同时把新旧值都传入:
value
(新值)和this.value
(旧值)不相等value
(新值)是一个对象Watcher
是一个deep Watcher
回到刚刚的for
循环,watcher.run()
执行完后,还有一段 if
判断:
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;
}
}
这段逻辑是Vue
针对死循环
抛出的警告,并终止运行。
我们通过一个例子来说明这种情况:
// App.vue
<template>
<div id="app">
{{ msg }}
<button @click="change">change</button>
</div>
</template>
<script>
export default {
data() {
return {
msg: 1
}
},
methods: {
change() {
this.msg++;
}
},
watch: {
msg() {
this.msg++;
}
}
}
</script>
在这个例子中,我通过 watch
属性来观察 msg
,并且在触发的回调又修改了 msg
,这样就造成了死循环,此时 Vue
会抛出一个警告:
这段警告也正是 if
逻辑中的那一段警告。这样 for
循环所有逻辑都分析完了,回到 flushSchedulerQueue
函数,在 for
循环执行完后,还剩下这一段:
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice();
const 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");
}
这里的 activatedChildren
和 callActivatedHooks
函数是和 keep-alive
相关的,所以会放在后面 keep-alive
的章节再来介绍。updatedQueue
则是拿到 queue
的一个副本,然后执行 callUpdatedHooks(updatedQueue)
。我们来看 callUpdatedHooks
的定义:
// src/core/observer/scheduler.js
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");
}
}
}
可以看到,callUpdatedHooks
会遍历 queue
中的 Watcher
,如果是一个渲染 Watcher
并且实例已经 mounted
且未 destroyed
,则调用生命周期钩子函数 updated
。
回到 flushSchedulerQueue
函数,这里还调用了 resetSchedulerState
函数:
// src/core/observer/scheduler.js
/**
* Reset the scheduler's state.
*/
function resetSchedulerState() {
index = queue.length = activatedChildren.length = 0;
has = {};
if (process.env.NODE_ENV !== "production") {
circular = {};
}
waiting = flushing = false;
}
resetSchedulerState
函数其实就是把相关变量恢复到初始值,同时将队列清空。
这样派发更新的整个流程就基本分析完了。
总结
到这里,我们已经把数据变化是怎么引起页面重新渲染的
的流程分析完了,总结一下大概就是:
- 派发更新,实际上就是当数据发生变化的时候,会触发
setter
,然后遍历在收集依赖过程收集到的观察者Watcher
,然后触发它们的update
方法 Vue
并不是简单的每次触发setter
就遍历Watcher
,而是会先把Watcher
推入到队列中,然后在下一个tick
执行flush
方法。