eventloop 浅析
浏览器主线程用 for 循环,不断地从消息队列中取出任务并执行任务
Micro-Task 与 Macro-Task
事件循环中的消息队列有两种:macro(宏任务)队列和 micro(微任务)队列。
常见的 macro-task 比如: script(整体代码)、 I/O 操作、UI 渲染、setTimeout、setInterval、 setImmediate等。
常见的 micro-task 比如: Promise、MutationObserver(监听DOM 变化),process.nextTick(node) 等。
示例
1次宏任务示例
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:pink';
2次宏任务示例
document.body.style = 'background:blue';
setTimeout(()=>{
document.body.style = 'background:black'
},200)
微任务示例
document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(() => {
console.log(2);
document.body.style = 'background:pink'
});
console.log(3);
整体过程
-
初始状态:调用栈空。micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)。
-
全局上下文(script 标签)被推入调用栈,同步代码执行。在执行的过程中,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程。
-
上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的(如下图所示)。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。
-
**执行渲染操作,更新界面 **
-
检查是否存在 Web worker 任务,如果有,则对其进行处理 。
(上述过程循环往复,直到两个队列都清空)
Vue状态更新手法:nextTick
Vue 每次想要更新一个状态的时候,会先把它这个更新操作给包装成一个异步操作派发出去。这件事情,在源码中是由一个叫做 nextTick 的函数来完成的:
/ Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
var timerFunc;
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
// 微任务
timerFunc = function () {
p.then(flushCallbacks);
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
// 微任务
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
// 宏任务
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
// 宏任务
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
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
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
参考资料
掘进小册 前端性能优化原理与实践 https://juejin.cn/book/6844733750048210957
「硬核JS」一次搞懂JS运行机制 https://juejin.cn/post/6844904050543034376#heading-22
https://github.com/lgwebdream/FE-Interview/issues/26