在vue官网深入响应式原理章节中,作者写到Vue是异步执行DOM更新的。官网的原话是“只要观察到数据变化,Vue将开启一个队列,并缓存在同一事件循环中发生的所有数据改变。...... 然后在下一个的事件循环tick中,Vue刷新队列并执行实际(已去重的)的工作。”看了官网的介绍还是有点懵圈,所以看了下vue数据响应的部分源代码。这里只分析涉及事件循环部分的代码。下一篇文章中会详细分析数据响应部分的源码。
源码(v2.3.4)
// 每次数据改变的时候会执行update函数
Watcher.prototype.update = function update() {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
// 这个函数做的一部分事情就是缓存watcher,可以理解为‘缓存在同一事件循环中发生的所有数据改变’
queueWatcher(this);
}
}
复制代码
这里只分析异步处理数据变化,即queueWatcher
函数。这个函数的代码如下:
function queueWatcher(watcher) {
var id = watcher.id;
// 忽略相同的watcher
if (has(id) == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
// 如果已经在flushing,则将watcher添加到最近执行的watcher的后面,会立即执行。
var i = queue.length - 1;
while(i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
if (!waiting) {
waiting = true;
// nextTick函数作用是将flushSchedulerQueue函数的执行放在下一个事件循环中。
nextTick(flushSchedulerQueue);
}
}
}
复制代码
我们来看一下nextTick函数,这个函数的作用即是将传入的回调函数放在下一次事件循环中执行。
// Defer a task to execute it asynchronously.
var nextTick = (function() {
var callbacks = [];
var pending = false;
var timerFunc;
function nextTickHandler() {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
// 这里是实现下次事件循环中执行的关键,后续会介绍 Promise、MutationObserver、setTimeout
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
var logError = function (err) { console.error(err); };
timerFunc = function() {
p.then(nextHandler).catch(logError);
if (isIOS) { setTimeout(noop); }
};
} else if (typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
var counter = 1;
var observer = new MutationObserver(nextTickHandler);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else {
timerFunc = function() {
setTimeout(nextTickHandler, 0);
}
}
return function queueNextTick(cb, ctx) {
var _resolve;
callbacks.push(function() {
if (cb) {
try{
cb.call(ctx);
}catch(err) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
// nextTick返回Promise对象,实现了nextTick().then(cb) 调用的功能
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function(resolve, reject) {
// 这里对_resolve赋值,将在callbacks数组里的函数执行时调用。
_resolve = resolve;
})
}
}
})();
复制代码
nextTick
函数里通过timerFunc
函数调用nextTickHandler
循环执行callbacks
队列里的函数。关键在timerFunc
函数,引用官网的话就是“Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MutationObserver,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替”。那么为什么这三种方式可以将nextTickHandler推迟到下次事件循环中执行呢?
这里涉及到JavaScript引擎的事件循环(Event Loop)了。
事件循环(Event Loop)
JavaScript只在一个线程上运行,也就是说同时只能执行一个任务,其他的任务需要等待当前任务完成后才能执行。这样带来的问题是,当执行比较耗时的任务时,会阻塞后续任务的执行,造成页面卡顿。为了防止一些非常耗时操作(比如I/O操作、ajax)阻塞后续代码执行,JavaScript采用了事件的机制。比如当执行ajax时,原本任务会继续执行,当等到ajax返回数据后,再执行ajax里指定的回调函数。一些I/O的操作也是类似。
消息队列,JavaScript引擎提供消息队列用于维护各类事件消息(各种事件,ajax状态改变事件,promise,setTimeout)。新的消息进入队列后,会自动排在队列的尾端。当执行栈上的任务执行完成,就会取出消息队列里的第一个消息,并且将对应的回调函数放入执行栈中执行。一旦执行栈中的任务全部执行完成,会再次取出消息队列里的第一条消息,执行回调函数,不断循环。
Wikipedia对Event Loop的定义是:“Event Loop是一个程序结构,用于等待和发送消息和事件。”个人的理解是当产生一个新的消息时,会将消息推入消息队列;在执行栈执行完当前任务时,会发送消息队列里的第一条消息,执行对应的回调函数。
所有的任务分为两种,一种是同步任务,一种是异步任务。
同步任务指的是,在JavaScript运行线程上排队执行的任务;异步任务指的是,不进入JavaScript运行线程,而是进入“任务队列(task queue)”的任务,只有当可以执行时,才进入线程的执行栈中执行。
虽然JavaScript只有一个线程来执行,但是并行的还有其他线程(比如,处理定时器的线程、处理用户输入的线程、处理网络通信的线程等等)。这些线程通过向任务队列添加任务,实现与JavaScript引擎线程通信。
Promise.then、MutationObserver和setTimeout,都是异步任务。经过以上的分析应该比较清楚异步任务的执行时机了。回调函数会在执行线程的任务完成后,再推入到执行栈中执行,也就是在会下一个事件循环中执行。
当然Promise.then和MutationObserver与setTimeout任务也是有区别的,Promise.then和MutationObserver产生的是“microTask”,setTimeout产生的是“task”。这两者是有差异的,后续会比较两者的差异。