事件循环

26 阅读4分钟

JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环。

JavaScript 在浏览器中的执行环境(也就是 JS 引擎)是单线程的,这个线程叫 主线程,主要负责:

  • 执行代码(同步、异步回调等)
  • 操作 DOM
  • 执行事件循环

JS 主线程不断的循环往复的从任务队列中读取任务,执行任务,这种运行机制称为事件循环(event loop) 事件循环(Event Loop)是 JavaScript 的执行机制,它确保同步代码优先执行,异步任务在适当的时机被处理。

JavaScript 是单线程语言,主线程首先执行所有同步任务。遇到异步任务(如定时器、网络请求、事件监听等),会将它们交给浏览器或 Node.js 的其他线程处理(如 Web APIs)。当这些异步操作完成后,相应的回调函数会被放入任务队列中。

事件循环机制不断从任务队列中取出任务执行。每次循环称为一次“tick”,每个 tick 中:

  1. 执行一个宏任务(如 setTimeoutsetIntervalI/OMessageChannel)。
  2. 在该宏任务执行完后,会立即清空微任务队列(如 Promise.thenMutationObserverqueueMicrotask)。

然后进入下一次循环,继续取出下一个宏任务,如此反复。

注意:不是先执行微任务,而是先执行一个宏任务,然后在该宏任务结束后立即执行所有微任务。

正确的执行顺序:

  • 从宏任务队列中取出一个宏任务执行(比如:整体脚本script,setTimeout回调等,每次执行一个)
  • 执行过程中产生的所有微任务(比如Promise.then)会被加入微任务队列
  • 在这个宏任务执行完成之后,立即执行所有微任务队列中的任务
  • 然后进入下一轮的事件循环,重复这个过程,直到所有的宏任务执行完成。

image.png 参考文章:js事件循环流程图 流程图模板_ProcessOn思维导图、流程图

练习题1

console.log("start");
setTimeout(() => {
    console.log("children2")
    Promise.resolve().then(() =>{
        console.log("children3")
    }) // 成功后的回调  属于微任务
}, 0)

new Promise(function(resolve, reject){
    console.log("children4")
    setTimeout(function(){
        console.log("children5")
        resolve("children6")
    }, 0)
}).then(res =>{         // flag
    console.log("children7")
    setTimeout(() =>{
        console.log(res)
    }, 0)
})

第一轮:

  • 先输出宏任务(script脚本)
  • 遇到同步代码,执行start。
  • 遇到异步代码,丢进宏任务队列。
  • 遇到Promise立即执行,输出children4。
  • 又遇到一个setTimeout 直接又丢入到宏任务队列,第一轮宏任务执行完,且没有微任务

问:上面的 .then() (注释的flag处) 是第一轮宏任务循环的微任务吗? 不是!因为resolve 都没有执行,promise 的状态都还没有从pending改变,就不是第一轮的微任务。

第二轮: 先输出宏任务(setTimeout)输出children2,第二轮宏任务结束 开始微任务执行promise 中的.then() 输出 children3第二轮循环结束

第三轮: 接着又开始setTimeout 的宏任务,输出children5,微任务输出 children7。这里遇到一个宏任务 setTimeout,丢入宏任务队列。第三轮循环结束

第四轮:又开始新 setTimeout 宏任务,输出 res children6

等价写法

image.png

练习题2

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')
}, 0)
async1()
new Promise((resolve) => {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')

✅ 首先明确:

  • 一轮事件循环(tick)包括:

    • 取出一个宏任务(宏任务队列中)
    • 执行它的全部代码
    • 执行当前产生的所有微任务
    • 然后进入下一轮事件循环

🧠 事件循环分析:

第 1 轮事件循环(主线程) :宏任务 = 整个同步代码

执行顺序:

  1. console.log('script start') → ✅

  2. 注册 setTimeout → 宏任务入队 ✅

  3. async1() 执行:

    • 输出:async1 start

    • await async2()

      • 执行 async2() → 输出:async2
      • await 之后的 console.log('async1 end') → 加入微任务队列 ✅
  4. 创建 Promise:

    • 输出:promise1
    • then(...) → 加入微任务队列 ✅
  5. console.log('script end') → ✅

🔹 同步任务结束,本轮开始执行微任务队列

  • 输出:async1 end
  • 输出:promise2

⏱️ 微任务执行完毕,第一轮事件循环结束


第 2 轮事件循环:宏任务 = setTimeout

  • 输出:setTimeout

执行完,没有新的微任务,结束。


✅ 最终结论:

总共执行了 2 轮事件循环:
1️⃣ 第 1 轮:主脚本(同步 + 微任务)
2️⃣ 第 2 轮:setTimeout 回调(宏任务)

练习题3: `

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    new Promise(function (resolve) {
        console.log('promise1');
        resolve();
    }).then(function () {
        console.log('promise2');
    });
}
console.log('script start');
setTimeout(function () {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function (resolve) {
    console.log('promise3');
    resolve();
}).then(function () {
    console.log('promise4');
});
console.log('script end');
//script start, 
// async1 start, 
// promise1, 
// promise3, 
// script end, 
// promise2,
// async1 end,
// promise4, 
// setTimeout

` 参考文章:面试 | JS 事件循环 event loop 经典面试题含答案 - 知乎