JavaScript 的事件循环(Event Loop)是 JavaScript 实现异步编程的核心机制,它决定了代码的执行顺序,尤其是在处理异步任务(如网络请求、定时器、DOM 事件等)时。
理解事件循环是掌握 JavaScript 异步编程的关键。
JavaScript运行机制
JavaScript引擎在执行JavaScript代码时,会将任务分为两类:同步任务和异步任务。同步任务在主线程上执行,而异步任务则由任务队列中的事件循环机制异步执行。在异步任务完成后,就会将该任务对应的回调函数放入任务队列中,并等待主线程执行完当前所有的同步任务后再执行该回调函数。
JavaScript中的回调函数是一种特殊的函数,它作为另一个函数的参数传递进去,在该函数执行完特定的操作后被异步调用。
事件循环的核心概念
JavaScript 是单线程的(只有一个主线程执行代码),但浏览器或 Node.js 环境提供了多线程的异步 API(如网络线程、定时器线程等)。事件循环的作用是协调主线程与异步任务的执行顺序。
JavaScript 是单线程的,意味着同一时间只能做一件事情,但是并不意味着单线程就是阻塞,实现单线程非阻塞的方法就是事件循环。
核心组成部分:
- 调用栈(Call Stack) :执行同步代码的地方,遵循 “后进先出”(LIFO)原则。
- 回调队列(Callback Queue) :存放异步任务的回调函数(如
setTimeout、fetch的回调),遵循 “先进先出”(FIFO)原则。 - 微任务队列(Microtasks Queue) :优先级高于回调队列,存放 Promise 回调(
then/catch/finally)、queueMicrotask等。 - 宏任务队列(Macrotasks Queue) :即传统的回调队列,存放
setTimeout、setInterval、DOM 事件、fetch响应等。
事件循环的执行流程
- 执行同步代码:主线程从调用栈中依次执行同步代码,遇到异步任务时,将其交给对应的心搏线程处理(如定时器线程处理
setTimeout)。 - 处理异步任务:异步任务完成后,其回调函数会被放入对应的队列(微任务放微任务队列,宏任务放宏任务队列)。
- 执行微任务:当调用栈为空时,立即执行微任务队列中的所有任务(按顺序执行,直到清空)。
- 执行宏任务:微任务队列清空后,从宏任务队列中取第一个任务放入调用栈执行。
- 循环往复:重复步骤 3-4,形成事件循环。
流程图示意:
同步代码 → 调用栈清空 → 执行所有微任务 → 执行一个宏任务 → 调用栈清空 → ...(循环)
微任务与宏任务的分类
微任务(优先级高)
- Promise 的
then/catch/finally回调 queueMicrotask函数process.nextTick(Node.js 环境,优先级高于其他微任务)MutationObserver(监听 DOM 变化的回调)
执行机制:微任务队列在当前宏任务执行完毕后,会被立即执行,在微任务队列中的所有任务都执行完之前,不会执行下一个宏任务。
宏任务(优先级低)
setTimeout/setInterval- DOM 事件(如
click、scroll) fetch等网络请求的响应回调setImmediate(Node.js 环境)I/O操作(如文件读写,Node.js 环境)postMessage、MessageChanner
执行机制:在事件循环中,每次取出一个宏任务执行,执行完毕后,会去检查微任务队列,将微任务队列中的所有微任务执行完毕后,再取下一个宏任务执行。
事件循环(Event Loop)与宏任务、微任务的关系
- 执行栈执行同步代码,遇到异步任务时,根据类型将其放入对应的宏任务队列或微任务队列。
- 当执行栈为空时,先检查微任务队列。如果有微任务,依次执行所有微任务,直到微任务队列为空。
- 微任务队列执行完毕后,从宏任务队列中取出一个宏任务执行。
- 执行完这个宏任务后,再次检查微任务队列,重复上述过程,形成事件循环。
关键特性总结
- 单线程执行:同步代码依次执行,异步任务不阻塞主线程。
- 微任务优先:每次调用栈清空后,会先执行完所有微任务,再执行一个宏任务。
- 队列顺序:同一队列中的任务按 “先进先出” 执行。
- DOM 渲染时机:在微任务执行完毕、下一个宏任务执行前,浏览器可能会进行 DOM 渲染(这也是为什么微任务中修改 DOM 会同步反映)。
经典示例解析
通过代码示例理解执行顺序:
示例解析1
console.log('1'); // 同步代码
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务
}).then(() => {
console.log('4'); // 微任务
});
console.log('5'); // 同步代码
执行步骤:
- 执行同步代码
console.log('1')→ 输出1。 - 遇到定时器
setTimeout,将回调放入宏任务队列,后面执行 → 宏任务队列:[() => console.log('2')]。 - 遇到
Promise.resolve().then(),将第一个then回调放入微任务队列 → 微任务队列:[() => console.log('3')]。 - 执行同步代码
console.log('5')→ 输出5。 - 调用栈清空,执行所有微任务:
- 执行第一个微任务 → 输出
3,此时第二个then回调进入微任务队列 → 微任务队列:[() => console.log('4')]。 - 继续执行微任务 → 输出
4,微任务队列清空。 - 从宏任务队列取第一个任务执行 → 输出
2。 最终输出顺序:1 → 5 → 3 → 4 → 2
示例解析2
console.log(1)// 同步代码
setTimeout(()=>{
console.log(2)// 宏任务
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')// 同步代码
resolve()
}).then(()=>{
console.log('then')// 微任务
})
console.log(3)// 同步代码
执行步骤:
// 遇到同步代码 console.log (1) ,直接打印 1
// 遇到定时器,属于新的宏任务,留着后面执行
// 遇到 new Promise,这个是直接执行的,打印 'new Promise'
//.then 属于微任务,放入微任务队列,后面再执行
// 遇到同步代码 console.log (3) 直接打印 3
// 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
// 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2
最终结果:1->'new Promise'->3->'then'->2
由此可见,事件循环、微任务、宏任务关系,如下图:
常见面试题场景
setTimeout延迟不准:因为setTimeout回调需要等待调用栈清空和前面的任务执行完,实际延迟可能大于设定值。- Promise 与
setTimeout优先级:Promise 回调(微任务)总是比setTimeout(宏任务)先执行。 - 嵌套异步任务:内部的微任务仍会在当前宏任务执行完前优先执行。
async与await
async 是异步的意思,await 则可以理解为 async wait。所以可以理解 async 就是用来声明一个异步方法,而 await 是用来等待异步方法执行
async
async 函数返回一个 promise 对象,下面两种方法是等效的
function f() {
return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
return 'TEST';
}
await
正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值
async function f(){
// 等同于
// return 123
return await 123
}
f().then(v => console.log(v)) // 123
不管 await 后面跟着的是什么,await 都会阻塞后面的代码
流程分析
script start -> async1 start -> async2 -> promise1 -> script end ->
同步代码执行完成
执行加入到微任务队列中的微任务代码 -> async1 end
上一个微任务执行完成,开始下一个微任务 -> promise2
上一个宏任务都执行完成,开始下一个宏任务 ->settimeout
async function async1() {
console.log('async1 start')// 同步代码
await async2()
console.log('async1 end') // 阻塞 ,加入到微任务队列,跳出去执行同步代码
}
async function async2() {
console.log('async2')// 同步代码
}
console.log('script start') // 同步代码
setTimeout(function () {
console.log('settimeout')// 宏任务,先不执行,等微任务都执行完成后才执行
})
async1()
new Promise(function (resolve) {
console.log('promise1')// 同步代码
resolve()
}).then(function () {
console.log('promise2')// 微任务,加入到微任务队列
})
console.log('script end')// 同步代码
分析过程:
- 执行整段代码,遇到
console.log('script start')直接打印结果,输出script start - 遇到定时器了,它是宏任务,先放着不执行
- 遇到
async1(),执行async1函数,先打印async1 start,下面遇到await怎么办?先执行async2,打印async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码 - 跳到
new Promise这里,直接执行,打印promise1,下面遇到.then(),它是微任务,放到微任务列表等待执行 - 最后一行直接打印
script end,现在同步代码执行完了,开始执行微任务,即await下面的代码,打印async1 end - 继续执行下一个微任务,即执行
then的回调,打印promise2 - 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印
settimeout
所以最后的结果是:script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout