Javascript的事件循环

从一个示例开始

在查看解析之前,思考一下下面的代码会输出什么内容?

1
2
3
4
5
6
7
8
9
10
11
12
13
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function executor(resolve) {
console.log(2);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);

解析:

  • 首先 setTimeout 里的内容进入 Event Loop 的 macrotask,在下一个 Event Loop 中执行,所以暂不执行.
  • Promise里的函数是立即执行的,其中 resolve 中的任务会进入 Event Loop 的 microtask,所以最先输出 2 和 3.
  • 而后是Promise之后的语句,输出 5. 到这里立即执行的语句已经结束.
  • 下一阶段开始执行 microtask 中的任务,即 resolvethen 中的回调,故输出 4 .
  • 然后,开始下一轮循环,执行 macrotask 中的任务,所以是执行到 setTimeout 的回调输出 1.

其中要注意的是,Promise.then()里面的回调属于 microtask, 会在当前 Event Loop 的最后执行, 而 setTimeout 内的回调属于 macrotask, 会在下一个 Event Loop 中执行.

所以正确输出顺序是:

1
2
3
4
5
2
3
5
4
1

Event Loop

一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。

任务队列又分为macro-task(宏任务)与micro-task(微任务)。

  • macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。

  • micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)

事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。