先做个题: 下面日志应该以什么顺序出现?
`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');`
正确的答案是:
script start, scriptend, promise1, promise2, setTimeout
为什么会这样,要理解这一点,您需要了解事件循环如何处理任务和微任务。
每个线程都有自己的event loop,因此每个web worker都有自己的event loop,因此它可以独立执行,而同一来源上的所有窗口都共享一个event loop,因为它们可以同步通信。事event loop持续运行,执行队列中的任何任务。一个event loop有多个任务源,这些任务源保证了该源内的执行顺序(IndexedDB等规范定义了它们自己的顺序),但浏览器可以在循环的每个回合中选择从哪个源获取任务。这允许浏览器优先选择对性能敏感的任务,如用户输入事件;
Tasks(任务):
Tasks是让浏览器能够从其内部进入JavaScript/DOM领域,并确保这些操作按顺序进行。在任务之间,浏览器可能会呈现更新。从鼠标单击到事件回调需要安排任务,解析HTML也是如此,在上面的示例中,setTimeout。
setTimeout等待给定的延迟,然后为其回调安排新任务。这就是为什么在脚本结束后记录setTimeout,因为记录脚本结束是第一个任务的一部分,而setTimeout记录在单独的任务中。
Microtasks(微任务)
Microtasks是被安排用于在当前执行的脚本之后立即发生的事情,例如对一批操作作出反应,或者在不接受整个新任务的惩罚的情况下使某些事情异步。微任务队列在回调后处理,只要没有其他JavaScript在执行中,并且在每个任务结束时处理。在微任务期间排队的任何其他微任务都将添加到队列的末尾并进行处理。Microtasks包括mutation,observer,spromise,callbacks。
Promise完成,它就会将一个微任务排队等待其反动回调。这确保了Promise是异步的,即使承诺已经解决。then(yey, nay),立即将一个微任务排成队列。这就是为什么promise1和promise2会在脚本结束后记录,因为当前运行的脚本必须在处理微任务之前完成。promise1和promise2在setTimeout之前记录,因为微任务总是在下一个任务之前发生。
So, step by step:
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');
| Tasks | Run script |
| setTimeout callback | |
|---|---|
| Microtasks | Promise then |
| Microtasks | Promise then |
| JS stack | |
| Log | script start |
| Log | script end |
| Log | promise1 |
| Log | promise2 |
| Log | setTimeout |
有些浏览器有什么不同之处? 一些浏览器记录script start 、script end、setTimeout、promise1、promise2。他们在setTimeout之后运行promise回调。他们很可能将promise回调称为新任务的一部分,而不是微任务。 这些差异是因为,promises是ECMAScript的方法而不是HTML的;promises应该是微任务队列的一部分,并且有充分的理由。将promises视为 Tasks 会导致性能问题,因为与 Tasks 相关的事情(如渲染)可能会不必要地延迟回调。由于与其他任务源的交互,它也会导致非确定性,并且可能会中断与其他API的交互,未完待续......