JavaScript 异步编程完全指南

0 阅读9分钟

🚀 JavaScript 异步编程完全指南

从零彻底搞懂同步、异步、Event Loop、Promise 和 async/await

📌 为什么需要异步?

CPU 执行时间不能霸占一个任务太久,操作系统以几十毫秒为单位轮询分配给各个进程。

  • 🚫 如果 JS 没有异步机制,遇到网络请求、定时器这类耗时任务时,整个页面就会 "卡死"等待,用户无法做任何操作
  • 异步的存在,让 JS 可以把耗时任务 "扔到一边",先执行后面的代码,等耗时任务完成了再回来处理

💡 生活类比:你正在做饭(同步任务),突然快递到了(异步任务)。你不会傻站在门口等快递,而是继续做饭,等门铃响了再去取。异步就是这种 "不阻塞" 的智慧!


🧵 进程与线程

  • 🏢 进程:公司,有独立资源(PID process id),进程之间相互隔离
  • 👷 线程:员工,在进程内干活(TID thread id),同一进程内的线程共享资源
  • 🎯 主线程:JS 唯一的执行线程,负责所有同步代码
  • 🔧 子线程(浏览器提供):处理定时器、I/O、UI 事件等异步任务
    • ⚠️ JS 本身只有一个主线程,子线程由浏览器/Node 提供,JS 无法直接创建或操作子线程

📊 线程模型示意图

JS 是单线程语言
    ├── 👑 主线程:跑所有 JS 代码(同步任务)
    └── 🔧 子线程(由浏览器/Node/bun 提供,JS 本身不直接操作)
         ├── ⏰ 定时器线程(setTimeout/setInterval)
         ├── 🌐 I/O 线程(网络请求、文件读写)
         └── 🖱️ 事件触发线程(鼠标点击等 UI 事件)
  • ⚙️ C++ / Java 等系统级别语言有多进程架构,执行效率高,但手动管理线程很复杂,容易出 bug
  • ✨ JS 设计为单线程,天然避免了 **"多线程竞争"**和 "死锁" 等复杂问题

🤔 JS 能异步执行代码的原因

  • 📜 JS 本身是单线程语言,同一时间只能做一件事,代码从上往下顺序执行
  • 🌐 但浏览器/Node/bun 提供了多个 子线程,分别处理定时器、I/O、UI 事件等异步任务
  • 🤝 主线程负责执行同步代码,子线程负责处理异步任务,两者配合实现 "异步" 效果

⚠️ 注意事项:JS 的 "异步" 本质是一种 模拟出来的并发,不是真正的多线程并行。子线程是浏览器/Node 提供的底层能力,JS 代码本身始终运行在单线程上。


📋 JS 有哪些异步任务

异步任务说明常见 API
⏲️ setTimeout(延时器)延迟指定时间后执行一次代码setTimeout(fn, ms)
🔁 setInterval(定时器)每隔指定时间重复执行代码setInterval(fn, ms)
📡 I/O 操作网络请求、文件读写等耗时操作fetchXMLHttpRequestfs.readFile
🖱️ UI 事件用户与页面交互产生clickmouseoverkeydown 等事件监听

💡 fetch 与 Promisefetch 是浏览器内置的 API,用于发送网络请求。它的底层基于 Promise 实现,fetch() 返回的就是一个 Promise 对象,所以可以直接用 .then().catch() 处理请求结果或错误。

⚠️ 注意事项

  • setTimeout 的延迟时间是最短等待时间,不是精确时间,因为要等同步代码执行完才能轮到它
  • 🚀 setTimeout(fn, 0) 并不是立即执行,而是 "尽快执行",依然要等当前同步代码全部跑完

🔄 JS 执行机制(Event Loop)

  • 🚀 执行前端 script 或 node / bun 代码时,系统会启动一个进程,拥有独立的 PID,负责分配资源
  • 👑 进程启动一个 主线程,JS 因为足够简单,只有单线程

📍 执行顺序(六步法)

  1. 🏃‍♂️ 先把所有 同步任务 快速执行掉,让用户看到页面
  2. 👀 遇到定时器、fetch 请求、事件等耗时性的 异步任务,JS 不会原地等待
  3. 📝 把这些异步任务注册到 event loop(事件循环)中,跳过它们,先继续执行后面的同步代码
  4. ⏳ 等同步代码全部执行完毕后,再到 event loop 中把 已完成的异步任务 拿出来执行回调
  5. 🔁 重复上述过程 —— 这就是 Event Loop(事件循环)

🎯 宏任务与微任务

异步任务还可以进一步分为两类,它们的执行优先级不同:

类型特点常见 API示例
🏗️ 宏任务(Macrotask)每次 Event Loop 只取一个执行setTimeoutsetInterval、UI 事件定时器回调
微任务(Microtask)每次清空队列中 所有 微任务Promise.then/.catch/.finallyPromise 回调

📐 执行顺序规则

同步代码 → ⚡ 清空所有微任务 → 🏗️ 取一个宏任务 → ⚡ 清空所有微任务 → 🏗️ 取下一个宏任务...

💡 为什么 Promise 的回调比 setTimeout 先执行?

因为 Promise 的回调是 微任务,优先级比 setTimeout(宏任务)高。 即使 setTimeout 延时为 0,它也要等当前所有微任务执行完才轮到。

⚠️ 注意事项

  • 🔒 同步代码永远比异步代码先执行,即使 setTimeout 延时为 0 也一样
  • 🥇 微任务执行顺序优先于宏任务,所以在同一个代码块中,Promise 的 .then() 会比 setTimeout 的回调先执行
  • 🔁 如果微任务中又产生了新的微任务,会继续清空,不会等宏任务,可能导致 "微任务死循环" 阻塞页面

🎨 经典面试题

console.log('1️⃣ 同步代码开始');

setTimeout(() => {
  console.log('5️⃣ setTimeout 宏任务');
}, 0);

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

console.log('2️⃣ 同步代码结束');

// 输出顺序:1️⃣ → 2️⃣ → 3️⃣ → 5️⃣

🎮 控制执行流程

实际开发中经常遇到 "先做 A,再用 A 的结果做 B" 的场景:

  • 📱 场景举例:先调用 A 接口获取所有用户列表,再根据用户列表逐个调用 B 接口获取详情
  • 😵 如果没有好的异步控制机制,代码会写成 "回调地狱"(层层嵌套),难以维护
  • 🎁 Promise 的出现就是为了解决这个问题,让异步代码写起来像同步一样清晰

🏚️ 回调地狱示例(反面教材)

// 😱 噩梦般的回调地狱
getUser(function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      getProductInfo(details.productId, function(product) {
        console.log(product);
        // 再嵌套下去就要崩溃了...
      });
    });
  });
});

💖 理解 Promise

Promise 是 ES6 引入的异步编程解决方案,用来解决回调地狱问题,让异步任务的控制流程更清晰。

🏗️ 基本概念

  • 📦 实例化 Promisenew Promise(executor)
  • ⚙️ executor(执行函数)
    • 创建 Promise 时 立即执行(同步的)
    • 它是耗时性任务的容器,里面可以写异步代码(如 setTimeoutfetch
    • 会自动接收到两个函数参数:resolvereject

🎚️ 三种状态

Promise 实例有且仅有以下三种状态,一旦状态改变就不可再变

状态图标含义触发方式对应回调
pending进行中(初始状态)刚 new Promise 时
fulfilled已成功调用 resolve(value).then()
rejected已失败调用 reject(reason).catch()

🔗 链式调用

  • 🟢 resolve(result) — 任务成功解决,把结果传给 .then()
  • 🔴 reject(err) — 任务失败,把失败原因传给 .catch()
  • 🔚 .finally() — 无论成功失败都会执行,适合做收尾操作(如关闭加载动画)

📝 语法细节

const p = new Promise((resolve, reject) => {
  // executor 在这里写耗时任务
  // resolve(值)  → 让 Promise 变为 fulfilled 状态
  // reject(错误) → 让 Promise 变为 rejected 状态
  
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('🎉 数据加载成功');
    } else {
      reject('💥 出错了');
    }
  }, 1000);
});

p.then(值 => { 
  console.log('成功:', 值); 
})
.catch(错误 => { 
  console.log('失败:', 错误); 
})
.finally(() => { 
  console.log('🏁 执行完毕,清理工作'); 
});

⚠️ 注意事项

  • ⚡ executor 是 同步执行 的,不是异步的!它里面包含的 setTimeoutfetch 等才是异步代码
  • 🛑 resolvereject 只能调用其中一个,且只能调用一次。先调用的生效,后面的忽略
  • 🔄 .then().catch() 返回的都是新的 Promise 实例,所以可以无限链式调用
  • 🚨 如果没有 .catch() 捕获错误,rejected 状态的 Promise 会抛出一个 未捕获的异常,可能导致程序崩溃
  • 🧹 .finally() 不接受任何参数,无法知道前面是成功还是失败

✨ Promise 解决回调地狱

// 🎉 优雅的 Promise 链式调用
getUser()
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetails(orders[0].id))
  .then(details => getProductInfo(details.productId))
  .then(product => console.log(product))
  .catch(error => console.error('❌ 出错了:', error));

🎭 async / await — Promise 的语法糖

async/await 是 ES2017 引入的语法,本质上是 Promise 的 语法糖,目的是让异步代码写起来更像同步代码,进一步提高可读性。

📖 基本用法

  • 🏷️ async:加在函数前面,表示这个函数是异步函数,它 会自动返回一个 Promise
  • ⏸️ await:只能在 async 函数内部使用,后面跟一个 Promise 对象,会 等待这个 Promise 完成,然后直接拿到 resolve 的值
async function 获取数据() {
  const 结果 = await fetch('https://api.example.com/user');  // 等待 fetch 完成,拿到结果
  const 数据 = await 结果.json();                           // 等待解析完成
  console.log(数据);                                        // 拿到最终数据
  return 数据;
}

💡 用 async/await 替代 .then() 链,代码看起来就像从上往下执行的同步代码一样,更容易理解和维护。

🆚 对比示例

// 🔴 Promise 写法
function getUserData() {
  fetch('/api/user')
    .then(response => response.json())
    .then(data => {
      console.log('用户数据:', data);
      return fetch(`/api/orders?userId=${data.id}`);
    })
    .then(response => response.json())
    .then(orders => console.log('订单数据:', orders))
    .catch(error => console.error('错误:', error));
}

// 🟢 async/await 写法(更清晰!)
async function getUserData() {
  try {
    const userResponse = await fetch('/api/user');
    const user = await userResponse.json();
    console.log('用户数据:', user);
    
    const ordersResponse = await fetch(`/api/orders?userId=${user.id}`);
    const orders = await ordersResponse.json();
    console.log('订单数据:', orders);
  } catch (error) {
    console.error('错误:', error);
  }
}

⚠️ 注意事项

  • 🚫 await 只能在 async 函数内部使用,在外面用会报语法错误
  • 🔒 await 会阻塞它所在的 async 函数内的后续代码执行,但不会阻塞整个主线程(函数外的代码照常执行)
  • 📦 async 函数本身返回的是一个 Promise,所以也可以用 .then().catch()
  • 🥷 用 try...catch 可以捕获 await 中的异常,替代 .catch()

📚 总结速查表

概念核心要点
🧵 单线程JS 只有一个主线程,异步靠浏览器子线程配合
🔄 Event Loop同步 → 清空微任务 → 取一个宏任务 → 循环
微任务Promise.then/catch/finally,优先级高
🏗️ 宏任务setTimeout/setInterval/UI 事件,优先级低
💖 Promise解决回调地狱,三种状态:pending/fulfilled/rejected
🎭 async/awaitPromise 语法糖,让异步代码像同步一样写

🎉 恭喜你! 读完这篇文章,你已经彻底掌握了 JavaScript 异步编程的核心知识。从 Event Loop 到 Promise,再到 async/await,现在你可以自信地处理任何异步场景了!


📝 如果觉得文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬 支持一下~