同步执行#
单个观察者同步执行#
先看下同步执行的代码。
// ! 同步执行,不需要调用 nextTick 执行观察者 -> 用于 watch 自定义的观察者
else if (this.sync) {
this.run()
}如果设置了观察者的sync属性为true的话,会执行了观察者的run方法,执行同步更新。
怎么设置sync属性呢?看下面的示例代码。
new Vue({
watch: {
someWatch: {
handler() {
/* ... */
},
sync: true,
},
},
});在我们设置watch选项时,设置一个对象而不是函数,然后设置sync的值。
看下run方法的代码。
run() {
if (this.active) {
const value = this.get() // ! 获取值 -> 执行 getter,如果执行的是 updateComponent,返回 undefined
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 // ! 旧值更新为新值
// ! 处理用户定义的观察者,比如 watch $watch
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.active是否为true,如果为true,说明当前观察者是激活状态,可以执行更新。在我们删除观察者的时候,这个值会为false,表示观察者是非激活状态,不会执行更新。
首先,声明变量value存储调用this.get方法获取到新值,再判断新值和旧值是否不相同,新值是不是一个非null的对象,以及this.deep的值是否为true。如果满足其中的一个条件就会进行下面的逻辑。
声明常量oldValue存储旧值,然后把this.value更新为新值value。然后判断this.user的值是否为true,如果为true说明这个观察者是用户定义的。再调用this.cb函数去更新视图,调用时使用call方法,函数指向观察者所属的实例对象,并传入新值和旧值。
因为由用户定义的观察者,可能会出错,所以这里使用try...catch语句来执行回调函数cb,这样可以更好捕获可能出现的错误。在catch代码块中使用了this.expression正是前面定义的expOrFn函数的字符串转换值,为了更好的显示错误信息。
如果this.user为false,说明是这个观察者是 Vue 内部定义的,不过还是使用和用户定义的观察一样的方法调用它,只是少了捕获错误的代码。
注意,这里说的观察者执行,指的是观察者的回调函数的调用。观察者是一个实例对象,它是不能执行调用的。
全局观察者同步执行#
上面学习了单个观察者(的回调函数)同步执行过程,其实 Vue 还提供了一个全局配置属性Vue.config.async,默认是true,即默认 Vue 的所有观察者的回调函数都是异步执行的。
设置这个属性的代码在core/config.js文件中。
export default {
// ...
/**
* Perform updates asynchronously. Intended to be used by Vue Test Utils
* This will significantly reduce performance if set to false.
*/
async: true,
};如果在项目的入口文件把Vue.config.async的值设置为false,那么 Vue 的所有的观察者都是同步执行。
Vue 的观察者默认是异步执行的,异步执行的代码在core/observer/scheduler.js文件中。
export function queueWatcher(watcher: Watcher) {
// ...
if (!waiting) {
// ...
// ! 同步执行,主要用于非生产环境中测试代码
if (process.env.NODE_ENV !== "production" && !config.async) {
flushSchedulerQueue(); // ! 直接执行
return;
}
nextTick(flushSchedulerQueue); // ! 使用 nextTick 执行队列的回调函数
}
}把观察者放入的一个队列中,然后异步执行队列。如果设置了所有观察者都是同步执行的,那么它是怎么同步执行的呢?答案是按照id的顺序执行的。
看下队列中观察者是怎么执行的,查看flushSchedulerQueue函数的代码。
function flushSchedulerQueue() {
// ...
queue.sort((a, b) => a.id - b.id); // 队列中元素升序排序
}然后再看下订阅器dep是怎么触发依赖,即怎么通知观察者的。
查看notify方法的代码,在core/observer/dep.js文件中。
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) // ! id 升序排序
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}发现这里也是按照id的排序来通知观察者的。排序主要是为了保证观察者按照它创建时的顺序执行,避免错误。
异步队列更新#
如果没有设置观察者的执行模式,默认是异步执行的。
下面看下异步执行的代码,
// ! 异步执行,需要调用 nextTick 执行观察者 => 用于执行自动收集的依赖
else {
queueWatcher(this) // ! 使用异步队列更新
}调用queueWatcher函数,并把当前的观察者作为参数传入。
下面看下queueWatcher函数的代码,在core/observer/scheduler.js文件中。
let has: { [key: number]: ?true } = {};
let waiting = false; // ! 等待标识
let flushing = false; // ! 队列是否正在执行更新
const queue: Array<Watcher> = [];
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); // ! 从后面开始查找,插入到第一个比它 id 小的元素后面
}
// ...
}
}定义常量id存储观察者的id,然后判断has对象中是否存在key值和这个id相同,has是一个普通的对象。如果不存在,把id作为key存入到has对象中,id对应的值是true。
再判断flushing的值是否为false,如果为false,说明队列中的观察者没有在执行,这时候把观察者放入队列尾部。如果flushing的值为true,说明队列中有观察者正在执行,这时候就不能放在队列尾部,而是从队列的后面开始查找观察者,然后把当前观察者插入到第一个比它 id 小的元素后面,也就是按照id的顺序把当前观察者插入到队列的合适的位置中。
什么时候观察者在队列执行时还要入队呢?答案是计算属性观察者,因为计算属性更新值的时机和普通的响应性数据是不一样的。
继续看下面的代码。
// queue the flush
// ! waiting 标识,保证只执行一次
if (!waiting) {
waiting = true;
// ! 同步执行,主要用于非生产环境中测试代码
if (process.env.NODE_ENV !== "production" && !config.async) {
flushSchedulerQueue(); // ! 直接执行
return;
}
nextTick(flushSchedulerQueue); // ! 使用 nextTick 执行队列的回调函数
}代码中的waiting是一个开关,表示是否需要等待,它的默认值false,表示不需要等待。
当waiting为false时,先设置它为true,这样在执行if代码的过程中,如果函数里面再去调用queueWatcher函数就不会再去执行这个函数的if代码。因为无论递归执行多少次queueWatcher函数,函数里面的if代码只会执行一次。
在非生产环境中,如果设置了全局同步执行,直接调用flushSchedulerQueue函数同步执行队列的观察者。
注意,到这里还是同步执行。先不管flushSchedulerQueue是如何执行队列观察者的。否则使用nextTick函数异步执行flushSchedulerQueue函数,只有到了这里才是真正的异步执行。
nextTick#
在 Vue 的官方文档中,提供Vue.nextTick或者vm.$nextTick这两个 API 供我们异步执行函数。
它们都来自同一个nextTick函数,作用是:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
nextTick函数把同步执行的函数变成异步执行。
nextTick(fn);它的原理和定时器setTimeout是一样的。它在里面也封装了setTimeout函数。
在 Vue 2.6 以前的版本中,nextTick还会明显的区分宏任务和微任务中是怎么处理异步。但是从 2.6 版本开始就只是根据异步接口执行的优先级,按照顺序来尝试执行异步函数。
查看下面的代码,在core/util/next-tick.js文件中。
// ! 回调函数,明面上不分 microtasks 和 (macro)tasks 了,按照下面的优先级来实现
// ! 优先级(性能高低) Promise -> MutationObserver -> setImmediate -> setTimeout
let timerFunc;
// ! Promise
if (typeof Promise !== "undefined" && isNative(Promise)) {
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
if (isIOS) setTimeout(noop);
};
isUsingMicroTask = true;
} else if (
!isIE &&
typeof MutationObserver !== "undefined" &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
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;
// ! setImmediate
} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}首先声明变量timerFunc,用来存储异步执行函数的包装函数,包装异步执行的flushCallbacks函数。
按照异步函数的执行的优先级,先尝试使用Promise.resolve。如果不支持,再尝试使用MutationObserver。如果还是不支持,继续尝试使用setImmediate。最后使用setTimeout,这个异步接口基本所有的浏览器都会支持。通过这些异步接口,使flushCallbacks函数异步执行。
其中Promise.resolve接口的性能是最好的,使用它包装的异步函数在执行时会放在浏览器的微任务列表中,这个列表中的异步函数等待时间最短,执行最快。而setTimeout接口的性能是最差的。
看下flushCallbacks函数的代码。
const callbacks = []; // ! 回调函数组成的数组
let pending = false; // ! 队列是否等待执行,false 表示队列为空,不用等待
// ! 执行 callbacks 的所有的回调函数
function flushCallbacks() {
pending = false; // ! 重置 pending
const copies = callbacks.slice(0); // ! 拷贝副本
callbacks.length = 0; // ! 清空 callbacks
// ! 执行拷贝副本的所有回调函数
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}flushCallbacks的作用就是按照顺序执行callbacks数组中的所有的回调函数(观察者)。
下面看下导出的nextTick函数的代码。
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
// ! 把 cb 包装成一个函数,并放入 callbacks 数组中
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
// ! nextTick 没有传入回调函数时,提供一个 Promise 化的调用
if (!cb && typeof Promise !== "undefined") {
return new Promise((resolve) => {
_resolve = resolve;
});
}
}这个函数接受两个参数,第一个参数是回调函数cb,第二个参数是函数调用时的上下文环境。
声明变量_resolve,在用户使用nextTick时如果没有传入回调函数cb,在可以使用Promise时的环境时,会把变量_resolve赋值为Promise的resolve函数。
如果有回调函数cb,使用try...catch语句包装并执行这个函数,这样不但很好捕获错误,而且在cb没有值时还调用_resolve函数。另外,把这个包装的函数放入到数组callbacks中。
判断队列callbacks是否等待执行,如果pending为false时,说明数组中没有回调函数在等待执行,先设置pending为``true,表示数组中已经有回调函数,不用等待了,可以执行,然后调用timerFunc函数,也就是异步执行flushCallbacks`中的回调函数。
然后我们会发现在执行flushCallbacks函数时,一开始是把pending重置为false,因为函数接下来就会执行完所有的回调函数。
function flushCallbacks() {
pending = false; // ! 重置 pending
// ...
}小结#
在一般情况下, Vue 的所有观察者都是异步执行的,只有在测试环境或者其他的特殊场景才让观察者同步执行。另外同步执行时,在队列中都会根据观察者的id进行排序,然后按照顺序执行观察者的回调函数。