事件循环入门

8 阅读5分钟

注:本文是学习事件循环后的个人笔记,建议配合下列参考资料一起阅读。

资料来自


为什么要有事件循环

Javascript是一个单线程的编程语言。

所谓单线程,指语言同一时间只能执行一个任务,即如果执行任务a花费了很长的时间,那么后面的任务b、c、别无它法,只能等待直到任务a执行完毕。

演示代码:点击后整个页面会暂停,因为while阻塞了线程,导致后面的任务无法执行。

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8" />
  <title>事件循环演示</title>
  <style>
    body {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      margin: 0;
      background: #111;
      color: #eee;
      font-family: monospace;
      gap: 32px;
    }

    #track {
      width: 500px;
      height: 80px;
      border: 1px solid #333;
      border-radius: 8px;
      position: relative;
    }

    #box {
      width: 60px;
      height: 60px;
      background: #7c6aff;
      border-radius: 8px;
      position: absolute;
      top: 10px;
      left: 0;
    }

    button {
      padding: 12px 28px;
      background: #ff5f5f22;
      border: 1px solid #ff5f5f66;
      color: #ff5f5f;
      border-radius: 8px;
      font-family: monospace;
      font-size: 14px;
      cursor: pointer;
    }

    p {
      color: #666;
      font-size: 13px;
    }
  </style>
</head>
<body>

  <p>box 由 requestAnimationFrame 驱动</p>

  <div id="track">
    <div id="box"></div>
  </div>

  <button onclick="blockThread()">点击阻塞主线程 while(true)</button>

  <p id="status">主线程运行中</p>

  <script>
    const box = document.getElementById('box');
    const status = document.getElementById('status');

    let pos = 0;
    let dir = 1;

    function animate() {
      pos += 2 * dir;
      if (pos >= 440) dir = -1;
      if (pos <= 0) dir = 1;
      box.style.left = pos + 'px';
      requestAnimationFrame(animate);
    }

    requestAnimationFrame(animate);

    function blockThread() {
      status.textContent = '主线程已阻塞——页面冻结';
      // 让当前任务先结束,浏览器渲染文字更新
      // 再通过 setTimeout 把 while(true) 放进下一个任务
      setTimeout(() => {
        while (true) {}
      }, 0);
    }
  </script>

</body>
</html>

为了让单线程也能像多线程一样并发多个执行任务,诞生了事件循环机制


事件循环的组成

Pasted image 20260408234448.png

组成部分

  • 执行栈
  • 宏任务队列(消息队列)
  • 微任务队列

执行栈

用于跟踪某个函数的执行,每次调用一个函数,就把它压进栈顶;函数执行完,就从栈顶弹出。

流程示意图

function sayHi() {
  console.log("hello world");
}
function greeting() {
  sayHi();
}

greeting();
初始状态:[]
执行 greeting() 时:[greeting()]
执行 sayHi() 时:[greeting(), sayHi()]
// 控制台输出 hello world
sayHi() 执行完毕,出栈:[greeting()]
greeting() 执行完毕,出栈:[]

堆用于存放引用类型(对象、数组、函数等)的实际数据。栈里的变量只存一个指针,指向堆里的本体。 堆里的数据生命周期比栈长——只要还有引用指向它,GC 就不会清理它。闭包就是一个典型例子:外层函数的 Frame 出栈了,但被内层函数引用的变量依然活在堆里的闭包环境对象中。

流程示意图

function outer() {
  const x = 10;
  return function inner() {
    console.log(x);
  };
}

const fn = outer();
fn();

此时在堆中创建了(以下只是示意,实际上它们是堆里分开存储的独立对象(不同的内存地址上))

// 闭包环境对象
closureEnv: { x: 10 },
  
// inner 函数对象,持有指向闭包环境的引用
inner: function() { console.log(x) }  // → 指向 closureEnv

宏任务队列(消息队列)

用于存放宏任务,只有在前一个宏任务衍生的所有微任务被执行完后才会执行

常见宏任务:

  • 整个JS脚本
  • setTimeout / setInterval
  • 用户交互事件(点击、键盘输入等)
  • 网络请求完成(fetchXMLHttpRequest
  • 页面加载事件

微任务队列

用于存放微任务,只会在宏任务执行结束后才会执行,并且只会在执行完整个微任务队列(包括执行微任务过程中新产生的微任务)后才进入下一个宏任务。

常见微任务:

  • Promise.then / Promise.catch / Promise.finally
  • async/awaitawait 后面的代码)
  • queueMicrotask

任务执行顺序

在前文中了解了各个组件的职责,接下来看看它们如何协作运转:

JavaScript 引擎会不断重复以下循环:

  1. 宏任务队列中取出一个任务执行(初始时,整个脚本就是一个宏任务)。
  2. 执行过程中遇到函数调用,将其压入执行栈;遇到异步 API,将其回调放入对应的任务队列。
  3. 当前宏任务执行完毕后,立即依次清空微任务队列所有微任务(包括执行期间新产生的微任务)。
  4. 必要时进行页面渲染(Style、Layout、Paint)。
  5. 回到第 1 步,继续取下一个宏任务。

伪代码示意

while (true) {
  const macroTask = macroTaskQueue.dequeue();
  execute(macroTask);
  
  while (microTaskQueue.hasTask()) {
    const microTask = microTaskQueue.dequeue();
    execute(microTask);
  }
  
  if (shouldRender()) render();
}

练习题

题目

// 练习1:经典输出题,写出输出顺序
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出:?

// 练习2:复杂版
console.log('start');
setTimeout(() => {
  console.log('timeout1');
  Promise.resolve().then(() => console.log('promise inside timeout'));
}, 0);
Promise.resolve().then(() => {
  console.log('promise1');
  setTimeout(() => console.log('timeout inside promise'), 0);
});
console.log('end');
// 输出:?

// 练习3:async/await 版本
async function foo() {
  console.log('foo start');
  await bar();
  console.log('foo end');
}
async function bar() {
  console.log('bar');
}
console.log('script start');
foo();
console.log('script end');
// 输出:?

答案

// 练习1
1
4
3
2

// 练习2
start
end
promise1
timeout1 
promise inside timeout 
timeout inside promise

// 练习3(await bar() 后的 console.log('foo end') 相当于微任务。)
script start
foo start
bar
script end
foo end