在理解事件循环的图中看到了很多人都翻译了一篇文章,于是我也尝试着翻译一下,顺便加深自己的理解
文章翻译自 https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
翻译功底不高,所以可能读起来不是那么顺畅,有能力的可以自己阅读一下原版的文章,文章不难读,也可以结合其他人的翻译读。
正文
当我告诉我的同事 Matt Gaunt 我想写一篇关于微任务队列 (microtask queueing)和浏览器内部的事件循环执行机制的文章,他说:你尽管写,读了算我输。好吧,不管怎样,我已经写出来了,所以让我们坐下来享受阅读的过程,好吗?
事实上,如果你更想看视频,Philip Roberts 在 JSConf 上做了一个关于 event loop 的非常精彩的演讲演讲,但是这篇演讲中没有包含微任务(microtask),但是对于其余部分讲的特别好。无论如何,开始我们的表演…
来看看这一段 js 代码
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
你觉得 log 输出的顺序是什么样的
正确的答案是:script start
,script end
,promise1
,promise2
,setTimeout
可能因为浏览器的原因会有所不同。
Microsoft Edge, Firefox 40, iOS Safari 和桌面版 Safari 8.0.8 打印 setTimeout
在 promise1
和 promise2
之前。尽管这可能是浏览器之间竞争的结果。这确实有些奇怪,比如在 Firefox 39 和 Safari 8.0.7 表现始终相同。
发生了什么
为了理解它我们需要知道事件循环(event loop)如何处理任务(tasks)和微任务(microtask)。第一次接触到这些东西的时候可能会让你有些头疼,深呼吸…
每一个线程都有它自己的事件循环,所以每一个 web worker 都有它自己的事件循环并且它们可以各自独立的执行。而所有同源窗口共享同一个事件循环,因为它们可以同步通信。事件循环不断执行任务队列中的任务。事件循环有多种任务源,这保证了任务源的执行顺序(IndexedDB 等规范定义了他们自己的执行顺序),但是浏览器在每次循环的过程中需要挑出当前循环下需要执行的任务。这允许浏览器让一些敏感的任务例如用户的输入可以获得优先执行权。
任务(task)是事先安排好的,所以浏览器可以从它的内部进入 javascript / DOM 域中保证这些行为有序执行。在任务之间,浏览器可能会更新视图。从鼠标点击到事件回调,再到 html 的渲染,以及上面例子提到的 setTimeout
这些都需要任务调度。
setTimeout
等待了它需要延迟的时间后就会创建一个回调的新任务,这就是为什么 setTimeout
打印在 script end
之后,因为打印 script end
是第一部分的任务,打印 setTimeout
是单独分离出来的任务。好了,我们基本上了解了,但是我希望你能继续读下去…
微任务(Microtasks)经常被安排在当前执行脚本之后,比如执行一些批量操作,或者让一些任务变成异步任务在没有其他新任务的情况下。在没有其他 javascript 在执行中时,就开始将微任务中的任务添加到事件队列中。在微任务执行时任何其他微任务都会被添加到队列末尾进行处理。微任务包括 mutation observer 的回调,还有上面的例子所示的 promise 回调。
当一个 promise 处理完时,或者它已经处理完,它就会在这个队列中插入异步的回调。这保证了 promise 回调是异步的即使 promise 已定型。所以调用 .then(yey, nay)
是不会立即执行的,这就是为什么 promise1
和 promise2
会在 script end
后打印出来,因为只有在当前脚本执行完成之后微任务才会被执行。也因为微任务通常在下一个任务(tasks)执行之前被执行,所以 promise1
和 promise2
会在 setTimeout
之前打印
(这里有个演示每一步的怎么做的,对理解很有好处,可以在原文里面演示一下)
为什么有些浏览器表现不同
一些浏览器打印 script start
,script end
,setTimeout
,promise1
,prmose2
。它们执行 promise 函数的回调在 setTimeout
之后。有可能因为它们将 promise 的回调当成了新一轮的任务而不是一个微任务。
某种程度上可以理解这件事,promise 来自于 ECMA 标准而不是 HTML。ECMA 标准中有 ‘jobs’ 的概念,跟 microtasks 很相似,然而,仅仅通过一些类似邮件的讨论,这两者的区别并不是那么清晰。但是一般来说,都公认 promise 应该是 microtask 的一部分,而且确实比较好。
把 promise 当做任务会导致一些性能问题,比如回调可能没必因为任务相关的东西比如渲染而延迟。也可能导致因为与其他任务交互而产生的非确定性,并且可能会破坏与其他 API 的交互。
(翻译功底实在有限,大家可以参考这篇文章 [译] 深入理解 JavaScript 事件循环(二)— task and microtask)
下面我用自己的理解解释一下上面的代码吧
引用一张别人的图片
javascript 是单线程的,执行的时候有个主线程,执行 javascript 主线程中的代码,当遇到 setTimeout 时,将它的回调加入到 macro task 队列中,遇到 promise 等微任务时,将它们加到 微任务队列中,当主线程中任务都执行完毕以后,先将微任务队列中的任务加入主线程,然后执行该任务,当主线程再次没有任务之后,若微任务列表还有任务,再将微任务列表中的任务加入到主线程,依次类推
当微任务列表中所有任务都执行完毕,将 macro task 中的回调加入主线程,执行,执行完毕后,若 macro task 中还有任务,则继续将任务加入到主线程中,执行任务,依次类推,直到所有任务都执行完毕
其中微任务(micro task)包括
- process.nextTick
- promise
- Object.observe
- MutationObserver
macro task 包括
- setTimeout
- setInterval
- setImmediate
- I/O
- UI渲染
这里还有一篇文章 event loop js事件循环 microtask macrotask
讲的比较清楚。可以参考一下。