this.name = 1;
this.name = 2;
this.name = 3;
为什么只会执行一次?这是nextTick回调和watcher过滤的结果。当数据变化后,将watcher.update
函数存放进nextTick
的回调数组中,并且会做过滤。通过watcher.id来判断回调数组中是否已经存在这个watcher的更新函数,如果不存在才会push。之后next遍历这个无重复id的回调数组,便会执行更新。
因此当3次修改同一个数据时,仅会有一个watcher.update
进入回调数组,从而页面只更新一次。
常见的宏任务
- setTimeout
- setInterval
- setImmediate
- script
- MessageChannel
常见的微任务
- Promise
- MutationObserver
- process.nextTick
nextTick涉及到的点
- 任务队列 callbacks
- 任务队列执行函数 flushCallbacks
- 控制(宏任务,微任务)注册标记位 pending
- 宏任务,微任务
宏任务和微任务的特点
- 二者都是异步
- 二者会被注册到不同的任务队列中
- 宏任务不是一次性就全部清空,而是执行一个宏任务时,然后去清空一次微任务队列
Vue在2.6版本中,去掉了宏任务,仅仅保留下了微任务,Vue内部有两个函数,macroTimerFunc
用于注册宏任务,microTimeFunc
用于注册微任务。Vue内部借助了setImmediate
, MessageChannel
, setTimeout
来实现macroTimerFunc。
if (setImmediate存在) {
macroTimerFunc = function() {
setImmediate(flushCallbacks);
}
} else if (MessageChannel存在) {
// messageChannel有两个端口,一个用来绑定回调,一个用来接收传递过来的数据
var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = flushCallbacks;
macroTimerFunc = function() {
port.postMessage(1);
}
} else {
macroTimerFunc = function() {
setTimeout(flushCallbacks, 0);
}
}
microTimerFunc主要借助Promise来实现。
if (Promise存在) {
var p = Promise.resolve();
microTimerFunc = function() {
p.then(flushCallbacks);
}
} else {
microTimerFunc = macroTimerFunc;
}
Vue维护了自己的任务队列,配合宏微任务,主要目的为
- 减少了宏微任务注册
- 加快异步代码的执行
- 避免频繁更新
当我们使用this.$nextTick(cb)
是,这个cb回调会被放入到callbacks
中,那我们就必须要一个方法来遍历这个callbacks, 来逐个执行其中存放的函数,这个方法就是flushCallbacks
。下面我们主要来看看这个flushCallbacks到底做了什么
var callbacks = [];
var pending = false;
function flushCallbacks() {
pending = false;
// 浅拷贝
var copies = callbacks.slice(0);
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
nextTick
控制所有的异步代码,只注册一个宏微任务,那是如何控制的?
Vue.nextTick = function (cb, ctx) {
callbacks.push(function() {
cb && cb.call(ctx);
});
// 通过pending来确定是否需要注册宏微任务
// 当第一次注册的时候,把pending设置为true,表示任务队列已经开始了,同一时期内无需注册了
// 在任务队列执行完毕之后再把pending设置为false
if (!pending) {
pending = true;
// useMacroTask 用来控制是宏任务还是微任务
useMacroTask ? macroTimerFunc() : microTimerFunc();
}
}
那Vue是如何控制多次更新同一个数据,最终实际只触发一次更新呢?这个需要提到watcher了。我们知道Vue中的data经过Object.defineProperty处理之后,会变成响应式的。当数据更新之后,会触发这个数据对应的set函数,而这个set函数通知相关的watcher进行更新,实际上就是调用了watcher.update
方法
Watcher.prototype.update = function() {
// 这里的queueWatcher对于多次赋值,一次更新起到了关键作用
queueWatcher(this);
}
function queueWatcher(watcher) {
var id = watcher.id;
// 如果是同一个watcher,那就不要再次进入宏微任务队列了,直接过滤掉
if (has[id] == null) {
has[id] = true;
// flushing 表示watcher更新队列正在执行更新
if (!flushing) {
// queue是更新队列,存放需要更新的watcher
queue.push(watcher);
} else {
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
}
// waiting=true表示已经把watcher更新`队列执行函数flushSchedulerQueue`注册到宏微任务上了
// 当所有的watcher更新完毕,waiting会被重置为false
if (!waiting) {
waiting = false;
// flushSchedulerQueue watcher更新队列的执行函数
nextTick(flushSchedulerQueue);
}
}
queueWatcher
主要做了两件事情
- 处理watcher到更新队列queue
- 注册
watcher更新队列的执行函数flushSchedulerQueue
到宏微任务队列
最后我们来看Vue是如何执行更新队列的
function flushSchedulerQueue() {
flushing = true;
var watcher;
// 升序排列watcher更新队列
// watcher的id越大,表示这个watcher越年轻,实例越是后面生成
queue.sort((a, b) => {
return a.id - b.id;
});
// 遍历watcher更新队列,然后逐个调用watcher更新
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
has[watcher.id] = null;
watcher.run();
}
// watcher更新队列完毕,重置状态
queue.length = 0;
has = {};
waiting = flushing = false;
}
事件回调执行的过程中,在JS主线程为空之后,异步代码执行之前,所有通过nextTick
注册的异步代码都是宏任务。
function add$1(event, handler) {
handler = withMacroTask(handler);
target$1.addEventListener(event, handler);
}
function withMacroTask(fn) {
return fn._withTask || (fn._withTask = function() {
useMacroTask = true;
var res = fn.apply(null, arguments);
useMacroTask = false;
return res;
})
}