Skip to content

事件循环(Event Loop)

引入背景

我们都知道,js的工作是操作dom元素并响应交互,例如点击按钮弹出界面,输入文字记录信息等等。这决定了js必须是单线程的。

用通俗易懂的话来说,就是js在同一时间只能做一件事情。

这会导致一些问题。如果碰到耗时操作(如网络请求、定时器),单线程会阻塞后续代码执行,导致页面卡死。

为了解决这个问题,事件循环机制应运而生。

事件循环工作流程

  1. 当网页打开时,浏览器加载了script中的代码,(或者node.js执行一个js文件时)整个脚本代码会被包装成一个宏任务

这个宏任务被放到了宏任务队列。

  1. 事件循环启动。取出第一个宏任务,执行里面的代码。同步代码压入调用栈,立刻执行,执行完弹出。异步代码会被挂起,交给浏览器或者其他线程处理,将来回调函数会被放在队列。具体放到哪里,取决于异步操作的类型:
    • 宏任务源:比如setTimeout,setInterval等,等当定时时间到了,这些会被放在宏任务队列。
    • 微任务源:比如Promise.then/catch/finally等,他们的回调会放到微任务队列里。

async/await 是promise的语法糖,await后面的代码等价于.then()会被放进微任务队列。

  1. 调用栈空了,这个宏任务结束了,此时,去清空微任务队列。

  2. 微任务队列清空之后,若为浏览器环境,浏览器可能会去渲染。是否需要重新渲染页面等等。

  3. 然后再取一个宏任务,开始新的一轮----------

工作流程例子

console.log('1');

setTimeout(() => {
    console.log('2');
    Promise.resolve().then(() => console.log('3'));
}, 0);

Promise.resolve().then(() => {
    console.log('4');
    setTimeout(() => console.log('5'), 0);
});

console.log('6');

首先,整个 <script> 代码作为第一个宏任务(记为宏任务 0)进入宏任务队列,事件循环启动。

1. 执行宏任务 0:

  • 同步代码 console.log('1') 压入调用栈,立刻执行,输出 1
  • 遇到 setTimeout,交给浏览器的定时器线程计时 0ms,计时结束后将其回调推入宏任务队列,记为宏任务 A
  • 遇到 Promise.resolve().then(...),将其回调推入微任务队列,记为微任务 A
  • 同步代码 console.log('6') 压入调用栈,立刻执行,输出 6

2. 宏任务 0 结束,清空微任务队列:

  • 执行微任务 A:输出 4
  • 微任务 A 中又遇到 setTimeout,交给定时器线程,0ms 后回调推入宏任务队列,记为宏任务 B
  • 微任务队列清空。

3. 取下一个宏任务 A:

  • 执行 console.log('2'),输出 2
  • 遇到 Promise.resolve().then(...),将其回调推入微任务队列,记为微任务 B

4. 宏任务 A 结束,清空微任务队列:

  • 执行微任务 B:输出 3
  • 微任务队列清空。

5. 取下一个宏任务 B:

  • 执行 console.log('5'),输出 5
  • 无新任务产生,结束。

最终输出顺序:1 → 6 → 4 → 2 → 3 → 5