前言
JavaScript是单线程的,即在同一时间内只能处理一个线程。但是现实场景中存在很多的异步操作,如网络请求,IO等,为了解决这些异步导致的效率问题,javascript就通过事件循环(Event Loop)机制来实现非阻塞的异步操作。 深入理解事件循环的执行过程有助于更好地掌握JavaScript异步编程,避免一些常见的陷阱,提高代码的性能和可维护性。同时,对事件循环的理解也为优化异步任务的执行顺序提供了思路,确保程序在处理异步任务时能够更为高效和灵活。
什么是事件循环
事件循环是JavaScript运行时环境中处理异步任务的一种机制。它确保代码在执行异步任务时能够保持响应性,而不会阻塞整个程序的执行。JavaScript事件循环包含两个关键组成部分:微任务队列和宏任务队列。
宏任务和微任务
- 宏任务: 宏任务是一些较为耗时的任务,它们会被放入宏任务队列中等待执行。常见的宏任务包括定时器(setTimeout、setInterval)、事件监听、I/O等。
宏任务会在每次事件循环的迭代中最早进入队列的任务执行。 - 微任务: 微任务是一些需要尽快执行的任务,它们会在当前任务执行完成后立即执行。常见的微任务包括Promise的回调函数、MutationObserver的回调和process.nextTick(Node.js环境)等。
微任务队列会在每个宏任务执行结束后清空。
事件循环过程
- 执行同步代码: 从宏任务开始,从上到下按顺序执行所有的同步代码,遇到异步任务时,将其回调函数添加到任务队列中,然后继续执行完当前宏任务中的所有同步代码。
- 执行微任务: 执行完同步代码后,检查微任务队列,如果有微任务则按顺序执行所有微任务的回调函数。
- 执行宏任务: 选择宏任务队列中最早进入队列的任务执行,直至宏任务队列为空。
- 重复: 重复以上步骤,不断循环执行,直至程序结束。
事件循环举例
这里举个笔试中常见的题目类型,即代码执行后控制台的输出内容是什么?
代码
<script>
console.log(1);
setTimeout(function () {
console.log(2);
}, 3000);
new Promise(function (resolve, reject) {
console.log(3)
resolve()
}).then(function () {
console.log(4);
setTimeout(function () {
console.log(5);
}, 0);
}).finally(() => {
console.log(6);
})
console.log(7);
setTimeout(function () {
console.log(8);
}, 0);
setTimeout(function () {
console.log(9);
}, 100);
new Promise(function (resolve, reject) {
console.log(10)
resolve()
}).then(function () {
console.log('a');
console.log(11);
})
</script>
输出结果
1
3
7
10
4
a
11
6
8
5
9
2 // 3秒后打印
代码循环分析
执行栈调入第一个宏任务:
每一个<script>标签为一个宏任务,机制以宏任务开始,执行同步代码,输出1,3,7,10,此时宏任务队列为:
// 队一
setTimeout(function () {
console.log(2);
}, 3000);
// 队二
setTimeout(function () {
console.log(8);
}, 0);
// 队三
setTimeout(function () {
console.log(9);
}, 100);
微任务队列为:
// 队一
(function () {
console.log(4);
setTimeout(function () {
console.log(5);
}, 0);
}).finally(() => {
console.log(6);
})
// 队二
function () {
console.log('a');
console.log(11);
}
同步任务完成,将微任务队列代码全部调入执行栈执行,输出 4 后遇到 setTimeout ,将其放入宏任务队列,遇到 finally 将其放入微任务队列,接着输出 a 和 11,执行栈代码执行完毕,此时宏任务队列为:
// 队一
setTimeout(function () {
console.log(2);
}, 3000);
// 队二
setTimeout(function () {
console.log(8);
}, 0);
// 队三
setTimeout(function () {
console.log(9);
}, 100);
// 队四
setTimeout(function () {
console.log(5);
}, 0);
微任务队列为:
finally(() => {
console.log(6);
})
继续将微任务队列中的代码调入执行栈执行,输出 6,至此,第一个宏任务执行结束。
执行栈调入第二个宏任务
当执行栈和和微任务队列都为空时,查看宏任务队列是否存在待执行代码,如果存在,调入执行。此时宏任务队列中还有4个任务排队,此时调入第一个宏任执行,发现设置时间未到,重新入队,至此,执行栈为空,微任务队列为空,宏任务队列变为:
// 队一
setTimeout(function () {
console.log(8);
}, 0);
// 队二
setTimeout(function () {
console.log(9);
}, 100);
// 队三
setTimeout(function () {
console.log(5);
}, 0);
// 队四
setTimeout(function () {
console.log(2);
}, 3000);
执行栈循环调入宏任务
重复上述循环,事件循环机制重新从宏任务队列中调入代码到执行栈中执行,遇到可执行的立马执行,不可执行的重新入队,直至全部队列情况。
总结
- 事件循环总是从宏任务开始执行同步代码,遇到异步代码按类型放入微任务队列和宏任务队列中;
- 同步代码执行完毕,首先查看微任务队列,有代码则调入执行,没有则同理查看宏任务队列;
- 重复上述步骤进行循环调度。