JavaScript的事件循环是它的并发模型的核心部分,使得JavaScript能够在单线程中处理异步操作。事件循环允许JavaScript在执行代码时,同时进行非阻塞的I/O操作(如网络请求、文件操作等)。这个概念对于理解如何高效地构建交互式Web应用程序是至关重要的。
事件循环的工作流程
JavaScript运行时环境包含一个待处理消息的队列。每个消息都关联着一个用以处理这个消息的函数。当堆栈(包含当前执行的所有代码)为空时,事件循环会从队列中取出一个消息,然后处理它。这个处理包括调用与消息关联的函数(及其调用堆栈)。这个函数执行完毕后,堆栈空出,事件循环继续处理下一个消息,这个过程持续不断。
宏任务(Macrotask)与微任务(Microtask)
事件循环中的任务可以分成两种:宏任务(如setTimeout、setInterval、I/O操作)和微任务(如Promise.then、MutationObserver)。两者的主要区别在于它们被执行的时机。
宏任务(Macrotask): 每次执行完一个宏任务,都会检查微任务队列是否有任务,如果有,则会先执行微任务队列中的所有任务,然后才会执行下一个宏任务。
微任务(Microtask): 当前任务执行完毕后立即执行的任务。所有微任务执行完毕之后,若存在渲染操作,则会执行渲染操作,然后继续回到宏任务的执行。
事件循环的步骤
- 执行全局脚本:加载脚本后,全局脚本开始执行。
- 执行宏任务:一旦全局脚本执行完毕,事件循环会从宏任务队列中取出一个任务执行。
- 执行微任务:在当前宏任务执行完毕后,会检查微任务队列,如果队列不为空,则执行队列中的所有微任务。这些微任务可能会继续添加新的微任务到微任务队列中。事件循环会一直执行微任务队列中的任务,直到队列清空。
- 渲染:完成宏任务和微任务后,渲染界面(如果有必要的话)。
- 回到步骤2:继续从宏任务队列中取出下一个任务,重复这个过程。
例子1
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');
执行顺序:
console.log('script start');
- ‘script start’ 打印到控制台。这是同步代码的一部分,所以它首先执行。
setTimeout(function() {
console.log('setTimeout');
}, 0);
setTimeout
是一个宏任务。尽管延迟是0,但这个setTimeout
回调(任务)不会立即执行;它被加入到宏任务队列等待执行。在当前执行栈(包括紧接着执行的微任务)执行完之前,setTimeout
的回调不会被执行。
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
-
Promise.resolve()
是立即解决的Promise,.then
回调(设置的第一个promise1
打印操作)被加入到微任务队列,将在当前宏任务的同步任务执行完毕后立即执行。 -
第二个
.then
(即promise2
打印操作)也会成为一个微任务,它将在第一个.then
执行后被加入到微任务队列。
console.log('script end');
- ‘script end’ 打印到控制台。这也是同步代码的一部分,紧随
'script start'
的打印之后执行。
至此,同步任务执行完毕。事件循环将会查看微任务队列,看是否有任务需要执行;在这种情况下,微任务队列中有两个由Promise产生的任务需要执行:
-
‘promise1’ 被打印到控制台。这是第一个微任务。
-
由于第一个
.then
的回调执行完毕,第二个.then
的回调(作为微任务)现在执行,‘promise2’ 被打印到控制台作为第二个微任务。
至此,当前宏任务中的所有微任务都已经被执行。事件循环现在回转到宏任务队列,准备执行下一个宏任务:
- ‘setTimeout’ 的回调函数现在执行,因为它是在开始时通过
setTimeout
提交的宏任务。‘setTimeout’ 被打印到控制台。
最终的执行顺序是:
- ‘script start’
- ‘script end’
- ‘promise1’
- ‘promise2’
- ‘setTimeout’
例子2
console.log('1');
setTimeout(function () {
console.log('2');
process.nextTick(function () {
console.log('3');
})
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5')
})
})
process.nextTick(function () {
console.log('6');
})
new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {
console.log('8')
})
setTimeout(function () {
console.log('9');
process.nextTick(function () {
console.log('10');
})
new Promise(function (resolve) {
console.log('11');
resolve();
}).then(function () {
console.log('12')
})
})