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 代码。希望本文能够为你在学习和工作中提供有益的参考!