JavaScript 的事件循环机制决定了代码的执行顺序,是理解异步编程的核心。为了帮助大家更全面地掌握事件循环,本篇文章将通过一个综合案例深入解析事件循环中同步任务、异步任务、宏任务与微任务的执行顺序。
1. JavaScript 事件循环机制概述
js是单线程的,一次只能执行一个任务,为了避免阻塞主线程,js通过事件循环机制来处理同步和异步代码。
- 同步任务:立即执行的任务,同步任务会直接进入到主线程中执行;
- 异步任务:异步任务进入任务队列等待主线程内的任任务执行完毕后,会去任务队列读取对应的任务,推入主线程执行,比如
ajax网络请求,setTimeout定时函数等
宏任务与微任务:异步任务可以分为宏任务和微任务,每次执行完一个宏任务之后,都会立即执行所有的微任务,然后再执行下一个宏任务,这样循环执行任务代码,直到循环完所有代码。
宏任务:整体代码块(如 script 标签中的代码)、setTimeout、setInterval;
微任务:包括 Promise 的回调函数、process.nextTick(Node.js)等
2. 综合案例:事件循环中的执行顺序
我们将通过一个综合案例来解释事件循环的执行顺序和宏任务、微任务的优先级。
console.log("script start");
setTimeout(() => {
console.log("setTimeout 1");
}, 0);
Promise.resolve().then(() => {
console.log("promise 1");
}).then(() => {
console.log("promise 2");
});
setTimeout(() => {
console.log("setTimeout 2");
}, 0);
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
new Promise((resolve) => {
console.log("promise 3");
resolve();
}).then(() => {
console.log("promise 4");
});
console.log("script end");
2.1 执行顺序分析
-
主线程执行同步代码:
console.log("script start"):立即执行,输出script start。setTimeout(..., 0):定时器回调被放入宏任务队列,稍后执行。Promise.resolve().then(...):then回调被放入微任务队列。setTimeout(..., 0):另一个定时器回调被放入宏任务队列。async1():调用async1函数,输出async1 start,然后遇到await,暂停执行后续代码,继续执行async2,输出async2。new Promise(...):立即执行构造函数,输出promise 3,then回调被放入微任务队列。console.log("script end"):立即执行,输出script end。
-
执行微任务队列:
- 执行第一个微任务队列中的
promise 1,输出promise 1。 - 执行第二个微任务队列中的
promise 2,输出promise 2。 - 执行
async1函数中await之后的部分,输出async1 end。 - 执行
new Promise的then回调,输出promise 4。
- 执行第一个微任务队列中的
-
执行宏任务队列:
- 执行第一个
setTimeout的回调,输出setTimeout 1。 - 执行第二个
setTimeout的回调,输出setTimeout 2。
- 执行第一个
2.2 综合案例的执行结果
最终输出顺序:
arduino
复制代码
script start
async1 start
async2
promise 3
script end
promise 1
promise 2
async1 end
promise 4
setTimeout 1
setTimeout 2
2.3 详细执行流程解析
-
同步代码执行阶段:
script start作为同步任务立即输出。setTimeout被放入宏任务队列,稍后执行。Promise.resolve().then(...)的then回调被放入微任务队列。async1()调用时,async1 start被立即输出,async2被调用,输出async2。await后面的代码被放入微任务队列。- 新的
Promise实例被创建,立即输出promise 3,then回调被放入微任务队列。 - 最后输出
script end。
-
微任务队列执行阶段:
- 执行微任务队列中的
promise 1,输出promise 1。 - 紧接着执行
promise 2,输出promise 2。 - 执行
async1中await之后的console.log("async1 end"),输出async1 end。 - 执行
new Promise的then回调,输出promise 4。
- 执行微任务队列中的
-
宏任务队列执行阶段:
- 执行宏任务队列中的第一个
setTimeout回调,输出setTimeout 1。 - 执行宏任务队列中的第二个
setTimeout回调,输出setTimeout 2。
- 执行宏任务队列中的第一个
3. 深入理解 async/await 与事件循环
async/await 是 Promise 的语法糖,它会在 await 语句后将后续代码放入微任务队列中。
3.1 案例:async/await 与事件循环的交互
javascript
复制代码
async function foo() {
console.log('foo start');
await bar();
console.log('foo end');
}
async function bar() {
console.log('bar');
}
console.log('script start');
foo();
console.log('script end');
执行顺序:
console.log('script start'):输出script start。- 调用
foo(),输出foo start,然后await bar(),调用bar(),输出bar。foo函数暂停,await之后的代码被放入微任务队列中。 console.log('script end'):输出script end。- 主线程空闲,开始执行微任务队列,输出
foo end。
最终输出:
sql
复制代码
script start
foo start
bar
script end
foo end
4. 常见事件循环陷阱
4.1 setTimeout 延迟的误解
javascript
复制代码
setTimeout(() => {
console.log('setTimeout 1');
}, 0);
for (let i = 0; i < 1000000000; i++) {} // 模拟长时间的同步操作
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
输出顺序:
arduino
复制代码
setTimeout 1
setTimeout 2
解释:尽管两个 setTimeout 都设置了 0ms 延迟,但由于主线程被长时间同步任务阻塞,定时器的回调会被延迟执行。
4.2 Promise 链的递归执行
javascript
复制代码
Promise.resolve().then(() => {
console.log("Promise 1");
Promise.resolve().then(() => {
console.log("Promise 2");
});
});
输出顺序:
javascript
复制代码
Promise 1
Promise 2
解释:Promise 1 的回调进入微任务队列并被执行,在 Promise 1 回调中创建的 Promise 2 的回调会在当前微任务队列执行完毕后执行。
5. 性能优化建议
- 避免主线程阻塞:将复杂的计算或大数据处理放在
Web Worker或Node.js的Worker Threads中处理。 - 合理使用微任务与宏任务:通过微任务处理优先级较高的任务,如使用
Promise和queueMicrotask。 - 分解复杂的异步任务:将复杂的异步任务分解成多个小任务,避免长时间阻塞主线程。
6. 结语
通过本文的综合案例解析,我们深入了解了 JavaScript 事件循环机制的执行顺序、宏任务与微任务的区别,以及 async/await 与事件循环的交互原理。理解事件循环机制能够帮助我们更好地编写高效、性能优良的 JavaScript 代码。希望本文能够为你在学习和工作中提供有益的参考!