前言
在前端开发中,事件循环(Event Loop) 是一个非常重要的概念。它决定了JavaScript代码的执行顺序,尤其是在处理异步任务时(比如定时器、网络请求、Promise等)。
今天,我们将深入探讨浏览器的事件循环机制,尤其是宏任务(Macro Task)和微任务(Micro Task)的区别与执行顺序,并补充 async/await 的知识点。
一、什么是事件循环?
JavaScript是单线程的,这意味着它一次只能执行一个任务。为了处理异步操作(比如定时器、网络请求等),浏览器引入了事件循环机制。
事件循环的核心思想是:不断地从任务队列中取出任务并执行。
事件循环的工作流程可以简单概括为以下几个步骤:
执行同步代码。
执行微任务队列中的所有任务。
执行一个宏任务。
重复上述过程。
二、宏任务和微任务
1. 宏任务(Macro Task)
宏任务是指那些由浏览器发起的任务,通常包括:
setTimeout
和setInterval
定时器DOM事件(如点击事件)
requestAnimationFrame
I/O操作(如文件读取)
setImmediate
(Node.js环境)
2. 微任务(Micro Task)
微任务是指那些由JavaScript引擎发起的任务,通常包括:
Promise
的then
和catch
回调
MutationObserver
(用于监听DOM变化)
process.nextTick
(Node.js环境)async/await 中的异步操作(本质上是Promise)
3. 执行顺序
事件循环的核心规则是:每次执行一个宏任务后,都会清空微任务队列。也就是说,微任务的优先级高于宏任务。
三、async/await 是什么?
async/await
是ES7引入的语法糖,用于简化Promise的使用。它让异步代码看起来像同步代码,更容易理解和维护。
1. async
函数
用
async
声明的函数会自动返回一个Promise对象。如果函数内有返回值,返回值会被包装成Promise的
resolve
值。如果函数内抛出错误,错误会被包装成Promise的
reject
值。
async function foo() {
return 'Hello';
}
foo().then(result => console.log(result)); // 输出: Hello
2. await
关键字
await
只能在async
函数中使用。
await
会暂停当前async
函数的执行,等待后面的Promise完成。如果Promise成功,
await
会返回Promise的resolve
值。如果Promise失败,
await
会抛出异常,可以通过try/catch
捕获。
async function bar() {
let result = await Promise.resolve('World');
console.log(result); // 输出: World
}
bar();
四、async/await 与事件循环
async/await
本质上是基于Promise的,因此它属于微任务。当 await
后面的Promise完成时,await
之后的代码会被放入微任务队列中,等待当前宏任务执行完毕后执行。
示例1:async/await 与微任务
console.log('同步代码开始');
async function asyncFunc() {
console.log('async函数开始');
await Promise.resolve();
console.log('await之后的代码');
}
asyncFunc();
console.log('同步代码结束');
输出结果:
同步代码开始
async函数开始
同步代码结束
await之后的代码
分析:
同步代码:首先执行所有的同步代码,输出
同步代码开始
和async函数开始
。await:
await
会暂停asyncFunc
的执行,将await
之后的代码放入微任务队列。同步代码继续:继续执行同步代码,输出
同步代码结束
。微任务:最后执行微任务队列中的任务,输出
await之后的代码
。
示例2:async/await 与宏任务、微任务的混合
console.log('同步代码开始');
setTimeout(() => {
console.log('setTimeout 宏任务');
}, 0);
async function asyncFunc() {
console.log('async函数开始');
await Promise.resolve();
console.log('await之后的代码');
}
asyncFunc();
Promise.resolve().then(() => {
console.log('Promise 微任务');
});
console.log('同步代码结束');
输出结果:
同步代码开始
async函数开始
同步代码结束
await之后的代码
Promise 微任务
setTimeout 宏任务
分析:
同步代码:首先执行所有的同步代码,输出
同步代码开始
和async函数开始
。await:
await
会暂停asyncFunc
的执行,将await
之后的代码放入微任务队列。同步代码继续:继续执行同步代码,输出
同步代码结束
。微任务:执行微任务队列中的任务,输出
await之后的代码
和Promise 微任务
。宏任务:最后执行宏任务队列中的任务,输出
setTimeout 宏任务
。
示例3:复杂的微任务和宏任务嵌套
console.log('同步代码开始');
Promise.resolve().then(() => {
console.log('Promise 微任务1');
setTimeout(() => {
console.log('setTimeout 宏任务1');
}, 0);
});
setTimeout(() => {
console.log('setTimeout 宏任务2');
Promise.resolve().then(() => {
console.log('Promise 微任务2');
});
}, 0);
console.log('同步代码结束');
输出结果:
同步代码开始
同步代码结束
Promise 微任务1
setTimeout 宏任务2
Promise 微任务2
setTimeout 宏任务1
分析:
同步代码:首先执行所有的同步代码,输出
同步代码开始
和同步代码结束
。微任务:然后执行微任务队列中的任务,输出
Promise 微任务1
,并在微任务中注册了一个新的宏任务setTimeout 宏任务1
。第一个宏任务:执行第一个
setTimeout
宏任务,输出setTimeout 宏任务2
。第二个微任务:在第一个宏任务执行后,清空微任务队列,输出
Promise 微任务2
。第二个宏任务:最后执行在微任务中注册的
setTimeout 宏任务1
,输出setTimeout 宏任务1
。
思考题:以下代码的执行顺序是?
console.log('同步代码开始');
// 模拟一个异步操作,内部包含宏任务和微任务嵌套
function asyncOperation() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('asyncOperation 中的 setTimeout 宏任务');
Promise.resolve().then(() => {
console.log('asyncOperation 中的 Promise 微任务');
setTimeout(() => {
console.log('asyncOperation 中嵌套的 setTimeout 宏任务');
}, 0);
});
resolve('asyncOperation 完成');
}, 0);
});
}
// 定义一个 async 函数
async function asyncFunction() {
console.log('async 函数开始');
const result = await asyncOperation();
console.log(result);
console.log('async 函数结束');
}
// 调用 async 函数
asyncFunction();
// 宏任务
setTimeout(() => {
console.log('外层 setTimeout 宏任务');
}, 0);
console.log('同步代码结束');
这道题就当做课后习题留给大家去完成了,欢迎在评论中留下你的答案!
五、总结
事件循环:JavaScript通过事件循环机制处理异步任务,确保代码的执行顺序。
宏任务:由浏览器发起的任务,如
setTimeout
、setInterval
等。微任务:由JavaScript引擎发起的任务,如
Promise
的then
回调、async/await
。执行顺序:每次执行一个宏任务后,都会清空微任务队列。微任务的优先级高于宏任务。
async/await:
async/await
是基于Promise的语法糖,属于微任务。它让异步代码看起来像同步代码,更容易理解和维护。
理解事件循环、宏任务、微任务以及 async/await
的执行顺序,对于编写高效的异步代码非常重要。希望这篇文章能帮你轻松掌握这些概念!