一文说清js的事件循环

33 阅读4分钟

前端的事件循环主要有三种情况:

  • 1.js的事件循环
  • 2.浏览器的事件循环
  • 3.nodejs的事件循环

JavaScript 事件循环(Event Loop)的执行流程

JavaScript 是 单线程 的,它通过 事件循环(Event Loop) 来管理同步任务、异步任务(包括宏任务和微任务)。


1. 任务分类

在事件循环中,任务分为 同步任务(Synchronous)异步任务(Asynchronous)

同步任务

  • 立即执行,直接进入 主线程(Call Stack)
  • 例如:普通函数调用、console.log()、变量声明等

异步任务

  • 任务队列(Task Queue) 处理,等待主线程空闲时执行
  • 分为 宏任务(Macro-task)微任务(Micro-task)

2. 宏任务(Macro-task)

宏任务主要包括:

  • setTimeout
  • setInterval
  • setImmediate(Node.js)
  • requestAnimationFrame
  • I/O 任务
  • UI 渲染

特点

  • 宏任务会进入宏任务队列,等待主线程执行完所有同步任务后再执行
  • 每次事件循环(Event Loop)都会 优先执行一个宏任务,然后再执行所有微任务

3. 微任务(Micro-task)

微任务主要包括:

  • Promise.then/catch/finally
  • MutationObserver
  • queueMicrotask
  • process.nextTick(Node.js)

特点

  • 微任务会进入微任务队列
  • 每次执行一个宏任务后,立即执行所有微任务

4. 事件循环(Event Loop)执行顺序

  1. 执行主线程的同步任务(Call Stack 里的代码)
  2. 执行所有微任务(Micro-task)
  3. 执行一个宏任务(Macro-task)
  4. 重复以上步骤

5. 示例代码

console.log('1'); // 同步任务

setTimeout(() => {
    console.log('2'); // 宏任务
}, 0);

Promise.resolve().then(() => {
    console.log('3'); // 微任务
});

console.log('4'); // 同步任务

执行流程

  1. console.log('1')同步任务,直接执行
  2. setTimeout()宏任务,加入宏任务队列
  3. Promise.then()微任务,加入微任务队列
  4. console.log('4')同步任务,直接执行
  5. 执行所有微任务console.log('3')
  6. 执行一个宏任务console.log('2')

最终输出

1
4
3
2

6. 加入多个宏任务 & 微任务的例子

console.log('A');

setTimeout(() => {
    console.log('B');
}, 0);

Promise.resolve().then(() => {
    console.log('C');
}).then(() => {
    console.log('D');
});

console.log('E');

执行流程

  1. console.log('A') (同步,直接执行)

  2. setTimeout() (宏任务,放入宏任务队列)

  3. Promise.then() (微任务,放入微任务队列)

  4. console.log('E') (同步,直接执行)

  5. 执行所有微任务

    • console.log('C')
    • console.log('D')
  6. 执行一个宏任务

    • console.log('B')

最终输出

A
E
C
D
B

7.Promise中存在async和await的情况

var a
var b = new Promise((resolve) => {
 console.log(1);
 setTimeout(() => {
  console.log(2);
   resolve(2)
 }, 1000)
}).then(() => {
 console.log(3);
}).then(() => {
 console.log(4);
}).then(() => {
 console.log(5);
})

a = new Promise(async (resolve) => {
 console.log(a)
 await b
 console.log(a)
 await (a)
 resolve(true)
 console.log(6);
})

console.log('end');

执行流程

阶段 1:同步代码执行

  1. 声明变量 a

    • var a 变量提升,初始值为 undefined
  2. 执行 var b = new Promise(...)

    • 同步执行 Promise 构造函数

      • 输出 1 
      • 设置 setTimeout(() => resolve(2), 1000)(这是一个 宏任务,1秒后加入队列)。
  3. 执行 a = new Promise(...)

    • 同步执行构造函数中的 async 函数

      • console.log(a):此时 a 尚未被赋值,输出 undefined 
      • await b:暂停执行,将后续代码包装为 微任务(记为微任务1) ,等待 b 完成。
  4. 执行 console.log("end")

    • 输出 end 

阶段 2:处理微任务队列(此时为空)

  • 当前没有可执行的微任务(b 尚未完成,微任务1 仍在等待)。

阶段 3:执行宏任务(1秒后)

  1. setTimeout 回调触发

    • 执行 resolve(2),将 b 的状态变为 fulfilled
    • 触发 b.then(() => console.log(3)),将它的回调加入 微任务队列(记为微任务2)

阶段 4:处理微任务队列

  1. 执行微任务2

    • 输出 3 
    • 触发下一个 .then(() => console.log(4)),加入微任务队列(记为微任务3)。
  2. 执行微任务3

    • 输出 4 
    • 触发下一个 .then(() => console.log(5)),加入微任务队列(记为微任务4)。
  3. 执行微任务4

    • 输出 5 
  4. 执行微任务1(之前暂停的 a 的构造函数)

    • 恢复 await b 后的代码:

      • console.log(a):此时 a 已被赋值为 Promise(状态为 pending),输出 Promise { <pending> } 
      • await a:等待 a 自身变为 fulfilled,将后续代码包装为 微任务(记为微任务5) ,但此时 a 仍为 pending,因此微任务5 被阻塞,无法加入队列。

阶段 5:后续事件循环

  • 微任务5 永远不会被加入队列,因为 a 的状态始终为 pending(resolve(true) 被 await a 阻塞)。
  • 事件循环发现没有其他任务(微任务和宏任务队列均为空),结束运行。

最终输出

1
[AsyncFunction: a]
end
2
3
4
5
Promise { <pending> }

8. 总结

  • 同步任务 直接执行
  • 同步任务执行后会执行所有的微任务
  • 微任务队列中没有更多的微任务会去执行一个宏任务
  • 宏任务执行后,再检查是否有更多的微任务
  • 如果有微任务,执行所有微任务
  • 事件循环会不断重复这个过程

这样,JavaScript 实现了异步执行的机制,同时保持单线程模型的运行方式