知识篇 -- JS事件循环与异步

Ray Shine 2024/2/24 JavaScript基础知识异步

JavaScript是一种单线程语言,这意味着它一次只能执行一个任务。然而,在Web开发中,我们经常需要处理耗时的操作,如网络请求、定时器、用户交互等。如果这些操作阻塞了主线程,页面就会变得无响应。为了解决这个问题,JavaScript引入了事件循环 (Event Loop) 机制,使得JavaScript能够以非阻塞的方式处理异步任务。

# 一、JavaScript的单线程特性 核心

  • 定义:JavaScript引擎只有一个主线程来执行所有代码。
  • 优点:避免了多线程编程中复杂的同步问题(如死锁、竞态条件)。
  • 缺点:耗时操作会阻塞主线程,导致页面卡顿。

为了在单线程模型下实现非阻塞的异步操作,JavaScript将任务分为两类:同步任务 (Synchronous Tasks)异步任务 (Asynchronous Tasks)

# 二、同步任务与异步任务 分类

# 1. 同步任务 阻塞

  • 定义:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务。
  • 特点:按照代码的顺序依次执行,阻塞主线程。
  • 示例:函数调用、变量赋值、算术运算等大部分代码。

# 2. 异步任务 非阻塞

  • 定义:不进入主线程,而是进入 任务队列 (Task Queue) 的任务。只有当主线程上的同步任务执行完毕,主线程空闲时,才会从任务队列中读取异步任务到主线程执行。
  • 特点:不会阻塞主线程,允许页面保持响应。
  • 常见异步操作
    • 定时器setTimeout, setInterval
    • 事件监听click, load, scroll 等DOM事件
    • 网络请求Ajax, Fetch
    • Promise
    • requestAnimationFrame

# 三、事件循环 (Event Loop) 的工作原理 机制

事件循环是JavaScript运行时环境(如浏览器或Node.js)中的一个核心组件,它负责协调同步任务和异步任务的执行。

基本流程

  1. 执行栈 (Call Stack):所有同步任务都在这里执行。当函数被调用时,它会被推入栈中;当函数执行完毕,它会被弹出栈。
  2. 任务队列 (Task Queue):异步任务的回调函数在满足触发条件后,会被放入任务队列中排队。
  3. 事件循环
    • 不断检查执行栈是否为空。
    • 如果执行栈为空,事件循环就会检查任务队列。
    • 如果任务队列中有任务,事件循环就会将队列中的第一个任务推入执行栈,使其执行。

示例

console.log("Start"); // 同步任务

setTimeout(() => {
    console.log("Timeout Callback"); // 异步任务(宏任务)
}, 0);

Promise.resolve().then(() => {
    console.log("Promise Callback"); // 异步任务(微任务)
});

console.log("End"); // 同步任务

预期输出

Start
End
Promise Callback
Timeout Callback

解释

  1. console.log("Start") 进入执行栈并立即执行。
  2. setTimeout 被调用,其回调函数被放入 宏任务队列
  3. Promise.resolve().then() 被调用,其回调函数被放入 微任务队列
  4. console.log("End") 进入执行栈并立即执行。
  5. 此时,执行栈为空。事件循环开始工作。
  6. 事件循环首先检查 微任务队列,发现 Promise Callback,将其推入执行栈执行。
  7. 微任务队列清空后,事件循环检查 宏任务队列,发现 Timeout Callback,将其推入执行栈执行。

# 四、宏任务 (MacroTask) 与微任务 (MicroTask) 进阶

在ES6及之后,任务队列被进一步细分为宏任务队列和微任务队列。

# 1. 宏任务 (MacroTask) 大任务

  • 来源setTimeout, setInterval, setImmediate (Node.js), I/O, UI rendering
  • 特点:每次事件循环只会从宏任务队列中取出一个任务执行。

# 2. 微任务 (MicroTask) 小任务

  • 来源Promise.then(), Promise.catch(), Promise.finally(), process.nextTick (Node.js), MutationObserver
  • 特点:在当前宏任务执行完毕后,下一个宏任务开始之前,会清空所有微任务队列中的任务。

事件循环的详细步骤

  1. 执行一个宏任务(从宏任务队列中取出)。
  2. 执行过程中如果遇到微任务,就将其添加到微任务队列中。
  3. 当前宏任务执行完毕,检查微任务队列。
  4. 如果微任务队列不为空,就依次执行所有微任务,直到微任务队列清空。
  5. 如果微任务在执行过程中又产生了新的微任务,也会被添加到微任务队列的末尾,并在当前轮次中被执行。
  6. 微任务队列清空后,渲染引擎可能会更新UI。
  7. 开始下一轮事件循环,重复步骤1。

示例

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

setTimeout(function() { // 宏任务1
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function() { // 微任务1
    console.log('promise1');
}).then(function() { // 微任务2
    console.log('promise2');
});

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

输出顺序

  1. script start (同步)
  2. script end (同步)
  3. promise1 (微任务1)
  4. promise2 (微任务2)
  5. setTimeout (宏任务1)

# 五、总结

事件循环是JavaScript异步编程的基石。通过将任务划分为同步和异步,并进一步细分为宏任务和微任务,JavaScript引擎能够在单线程环境下高效地处理各种操作,确保用户界面的流畅性和响应性。理解事件循环的工作机制,对于编写高性能、无阻塞的JavaScript代码至关重要。

最后更新时间: 2025/11/20 22:59:30
ON THIS PAGE