Skip to content

JavaScript 异步编程

> [!important] > - JavaScript 是单线程执行语言,但宿主环境提供了定时器、网络、I/O 等异步能力。 > - 事件循环的核心流程是:执行一个宏任务 -> 清空当前所有微任务 -> 浏览器可能渲染 -> 执行下一个宏任务。 > - Promise.then/catch/finallyawait 的后续逻辑,本质上都走微任务。 > - setTimeout(fn, 0) 不是立刻执行,而是尽快放到后面的宏任务队列中。 > - async/await 本质上是 Promise 的语法糖,await 等的是 Promise.resolve(表达式) 的结果。

1. 异步编程的实现方式

JavaScript 常见的异步编程方式有 回调函数Promiseasync/await,此外还要了解 Generator + co事件驱动Web Worker。实际开发里最常用的是 Promiseasync/await

为什么需要异步

  • JavaScript 主执行线程通常一次只能做一件事。
  • 如果网络请求、定时器、I/O、重计算都同步执行,页面和服务会被阻塞。
  • 异步的目标不是“多线程”,而是“不要阻塞当前主线程”。

常见方式对比

方式核心思路优点缺点面试定位
回调函数异步结束后执行回调简单直接容易嵌套、错误处理分散基础概念
Promise用对象表示异步结果可链式调用、统一错误处理复杂流程下代码仍可能偏链式高频
async/awaitPromise 的语法糖写法像同步、可读性高无依赖任务容易被写成串行高频
Generator + coyield 暂停,执行器继续推进历史上用于模拟同步流程现在基本被 async/await 取代了解
事件驱动通过事件触发回调适合发布订阅、流式场景状态追踪复杂Node 常见
Web Worker把计算放到其他线程不阻塞 UI通信成本高,不能直接操作 DOM扩展点

最常见的三种写法

回调函数

js
function getData(callback) {
  setTimeout(() => {
    callback(null, 'data');
  }, 1000);
}

getData((err, data) => {
  if (err) return console.error(err);
  console.log(data);
});

Promis

js
function getData() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('data'), 1000);
  });
}

getData().then((data) => {
  console.log(data);
});

async/await

js
function getData() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('data'), 1000);
  });
}

async function main() {
  const data = await getData();
  console.log(data);
}

main();

小结

  • 现在业务代码优先使用 Promiseasync/await
  • Generator + co 重点知道“历史上如何在 async/await 出现前解决异步流程控制”即可。
  • Web Worker 不是 Promise 体系的一部分,但它是浏览器里非常重要的异步/并发能力补充。

2. setTimeout、Promise、async/await 的区别

setTimeout 是计时器 API,解决的是“多久之后执行”; Promise 是异步结果对象,解决的是“异步结果如何表达和传递”; async/await 是 Promise 的语法糖,解决的是“异步流程如何更清晰地写出来”。

对比表

维度setTimeoutPromiseasync/await
本质宿主环境提供的计时器 API表示异步结果的对象Promise 的语法糖
解决问题延迟/调度异步状态与结果管理异步流程的可读性
调度位置宏任务.then/.catch/.finally 回调是微任务await 后续逻辑是微任务
是否表达异步结果依赖 Promise 表达
错误处理回调中自己处理.catch()try...catch
典型场景延时、轮询、超时链式调用、并发组合业务流程编排

高频易错点

  • setTimeout(fn, 0) 不是“马上执行”,而是“最早在当前同步代码和微任务之后执行”。
  • new Promise(executor) 里的 executor 会同步执行,不是异步执行。
  • await 不会阻塞整个线程,只会暂停当前 async 函数的后续逻辑。
  • await 后面的代码并不是同步接着跑,而是等当前值 resolve 后,以微任务形式恢复执行。

一道经典小题

js
console.log('start');

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

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

console.log('end');

输出顺序:

txt
start
end
promise
timeout

原因:

  • 同步代码先执行,输出 startend
  • Promise.then 是微任务,先执行
  • setTimeout 回调是宏任务,后执行

3. 对 Promise 的理解

Promise 是一个用来表示异步操作最终结果的对象。它把“回调什么时候执行”这件事,抽象成了“对象状态什么时候改变”,从而支持链式调用、值传递和统一错误处理。

Promise 的三个状态

  • pending:进行中
  • fulfilled:已成功
  • rejected:已失败

> [!tip] > fulfilled / rejected,这是更标准的术语。很多人口语里会说 resolved,但严格来说 resolved 不完全等于 fulfilled

Promise 的核心特性

  • 状态一旦从 pending 变成 fulfilledrejected,就不会再变。
  • then 每次都会返回一个新的 Promise,所以可以链式调用。
  • new Promise(executor) 中的 executor 会立即同步执行。
  • then/catch/finally 中的回调会进入微任务队列。
  • 如果在 then 里返回一个普通值,下一个 then 会拿到这个值。
  • 如果在 then 里抛出错误,后面的 catch 能接住。
  • 如果返回的是另一个 Promise,链会等待这个 Promise 结束。

Promise 解决了什么问题

  • 解决了多层回调嵌套导致的可读性问题。
  • 解决了异步结果难以向下传递的问题。
  • 解决了错误处理分散在各层回调里的问题。
  • 提供了组合多个异步任务的统一方式,比如 allraceallSettledany

常用实例方法

then

用于处理成功结果,也可以继续返回值或 Promise。

js
request()
  .then((res) => {
    return res.data;
  })
  .then((data) => {
    console.log(data);
  });

catch

用于统一捕获链路中的异常或 rejected 状态。

js
request()
  .then((res) => res.data)
  .catch((err) => {
    console.error(err);
  });

finally

无论成功还是失败都会执行,适合做清理动作。

js
request().finally(() => {
  console.log('loading done');
});

高频易错点

  • Promise 本身不能直接“取消”,通常是取消底层任务,比如配合 AbortController 取消 fetch
  • catch 不仅能捕获 reject,也能捕获前面 then 中抛出的异常。
  • then 的第二个参数可以处理失败,但更推荐把失败统一放到 catch 里。
  • 如果 Promise reject 之后没有被处理,运行时可能出现 UnhandledPromiseRejection 警告。

总结

Promise 本质上是异步编程的统一抽象。它的价值不只是“避免回调地狱”,更重要的是:统一表达异步结果、支持链式调用、让错误处理变得可收敛。

4. Promise 并发方法的区别与使用场景

Promise.all 是“全部成功才成功”; Promise.allSettled 是“全部结束都给你结果”; Promise.race 是“谁先落定用谁”; Promise.any 是“只要有一个成功就成功”。

对比表

API成功条件失败条件返回值典型场景
Promise.all全部 fulfilled任一 rejected成功值数组多请求并发,且结果都必须拿到
Promise.race第一个 settled 且 fulfilled第一个 settled 且 rejected第一个落定的结果/原因超时控制、抢最快结果
Promise.allSettled全部 settled不会 reject每项状态数组希望拿到所有请求结果,不想因单个失败中断
Promise.any任一 fulfilled全部 rejected第一个成功值多源兜底、容灾

Promise.all

特点:

  • 返回结果顺序和传入顺序一致。
  • 只要有一个失败,整体就会立即 rejected。
  • 但其他 Promise 并不会自动停止,只是聚合结果不再继续等。
js
const [user, profile] = await Promise.all([
  getUser(),
  getProfile(),
]);

适用场景:

  • 页面初始化并发请求多个接口
  • 多个请求之间没有依赖,但都必须成功

Promise.race

特点:

  • 谁先 settled,就采用谁的结果。
  • 这里的“先”不区分成功还是失败。
js
function withTimeout(promise, ms) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('timeout')), ms);
  });

  return Promise.race([promise, timeoutPromise]);
}

适用场景:

  • 请求超时控制
  • 多个镜像源抢最快结果

Promise.allSettled

特点:

  • 不会因为其中一个失败就整体失败。
  • 最终能拿到每一项的状态和值/原因。
js
const results = await Promise.allSettled([
  getA(),
  getB(),
  getC(),
]);

适用场景:

  • 批量任务汇总
  • 后台管理页批量加载多个模块,允许局部失败

Promise.any

特点:

  • 只要有一个成功,就整体成功。
  • 如果全部失败,才会 rejected。

适用场景:

  • CDN / 多数据源兜底
  • 主从接口容灾

高频追问

  • Promise.all 中一个失败了,其他任务会取消吗? 不会自动取消,只是 all 返回的聚合 Promise 会立刻 rejected。
  • Promise.all 的结果顺序按什么来? 按传入顺序,不按完成顺序。
  • raceany 有什么区别? race 看“第一个落定”,any 看“第一个成功”。

5. 对 async/await 的理解

async/await 是 Promise 的语法糖。async 让函数返回 Promise,await 让我们可以用更像同步代码的方式等待异步结果,但底层依然是 Promise 和微任务调度。

async 做了什么

  • async 函数一定返回一个 Promise。
  • return 值 等价于 return Promise.resolve(值)
  • throw error 等价于 return Promise.reject(error)
js
async function foo() {
  return 123;
}

foo().then((res) => {
  console.log(res); // 123
});

await 到底在等什么

await x 本质上等价于等待 Promise.resolve(x)

也就是说它可以等三种东西:

  • Promise
  • thenable 对象
  • 普通值
js
async function test() {
  console.log(await 1); // 1
  console.log(await Promise.resolve(2)); // 2
}

> [!tip] > 所以面试里如果被问“await 1 会怎样”,标准回答是: > 1 会先被包装成一个立即成功的 Promise,然后在当前 async 函数里继续往下执行。

async/await 的优势

  • 写法更接近同步流程,阅读成本低。
  • 错误处理可以直接用 try...catch
  • 依赖顺序清晰,适合表达复杂业务流程。
  • 调试体验通常比层层 Promise 链更好。

常见坑

1. 连续 await 可能把并发写成串行

js
const a = await getA();
const b = await getB();

如果 getAgetB 没有依赖,这样写会串行执行。更好的写法是:

js
const promiseA = getA();
const promiseB = getB();

const [a, b] = await Promise.all([promiseA, promiseB]);

2. forEach 不会等待 async

错误示例:

js
list.forEach(async (item) => {
  await save(item);
});

更合适的写法:

js
for (const item of list) {
  await save(item);
}

或者并发执行:

js
await Promise.all(list.map((item) => save(item)));

3. await 不会阻塞整个线程

它只会暂停当前 async 函数,外面的同步代码和事件循环照常运行。

用 Promise 的视角理解 async/await

js
async function getResult() {
  const data = await fetchData();
  return data;
}

可以粗略理解成:

js
function getResult() {
  return Promise.resolve(fetchData()).then((data) => {
    return data;
  });
}

> [!warning] > 面试里说“async/await 就是 Promise 的语法糖”没问题,但最好补一句: > 它没有改变 JavaScript 的异步模型,只是让 Promise 的写法更接近同步流程。

6. 任务队列、微任务和事件循环

事件循环就是 JavaScript 处理异步任务的调度机制。可以简单理解为:先执行当前宏任务中的同步代码,执行完后清空所有微任务,然后再进入下一轮宏任务。

先搞清几个概念

  • Call Stack:执行栈,负责执行当前同步代码。
  • 宿主环境 API:比如浏览器的定时器、网络 API,或者 Node.js 的 I/O API。
  • 宏任务队列:存放宏任务。
  • 微任务队列:存放微任务。

一轮事件循环的核心流程

  1. 执行当前宏任务中的同步代码
  2. 当前宏任务执行结束,立刻清空所有微任务
  3. 浏览器环境下,可能进行一次渲染
  4. 进入下一个宏任务

常见宏任务和微任务

宏任务

  • 整体 script 脚本
  • setTimeoutsetInterval
  • I/O
  • 用户交互事件
  • postMessage

微任务

  • Promise.thenPromise.catchPromise.finally
  • queueMicrotask
  • MutationObserver

> [!tip] > 在浏览器面试里,一般回答到这里就够了。 > 如果是 Node.js 面试,还可以补一句:process.nextTick 的优先级通常比 Promise 微任务还高。

经典执行顺序题

js
console.log('1');

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

Promise.resolve()
  .then(() => {
    console.log('3');
  })
  .then(() => {
    console.log('4');
  });

console.log('5');

输出顺序:

txt
1
5
3
4
2

原因:

  • 15 是同步代码,先输出
  • 两个 then 回调属于微任务,先于宏任务执行
  • setTimeout 回调属于宏任务,最后执行

为什么微任务优先级更高

  • 保证当前这一轮任务产生的“后续收尾逻辑”能尽快执行。
  • Promise 状态变更可以更快反映到后续逻辑里。
  • 避免必须等到下一轮宏任务才继续,提升一致性。

高频易错点

  • 事件循环不是“先清空所有宏任务,再清空所有微任务”,而是“每执行完一个宏任务,就清空一次微任务”。
  • 微任务如果不断递归追加,可能导致宏任务和页面渲染被延后。
  • await 后面的继续执行逻辑,本质上也是微任务。
  • setTimeout(fn, 0) 受最小延时、主线程占用等影响,不保证 0ms 就执行。

7. 回调地狱是什么,怎么解决

回调地狱指的是在处理多个有依赖关系的异步任务时,回调层层嵌套,导致代码越来越向右偏、错误处理分散、可读性和可维护性变差。

一个典型例子

js
getUser(userId, (err, user) => {
  if (err) return handleError(err);

  getOrders(user.id, (err, orders) => {
    if (err) return handleError(err);

    getDetail(orders[0].id, (err, detail) => {
      if (err) return handleError(err);

      console.log(detail);
    });
  });
});

为什么会出现

  • 多个异步任务之间存在严格依赖
  • 每一层都要手动传递结果
  • 每一层都要单独做错误处理
  • 业务一复杂,缩进就越来越深

解决方式

1. 先拆函数

即便还在用回调,也不要所有逻辑都写在一个匿名函数里。

2. 用 Promise 链改写

js
getUser(userId)
  .then((user) => getOrders(user.id))
  .then((orders) => getDetail(orders[0].id))
  .then((detail) => {
    console.log(detail);
  })
  .catch(handleError);

3. 用 async/await 改写

js
async function main() {
  try {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    const detail = await getDetail(orders[0].id);

    console.log(detail);
  } catch (error) {
    handleError(error);
  }
}
  • 不是“有嵌套就一定是回调地狱”,关键看是否形成了深层依赖、错误处理分散和控制反转。
  • Promiseasync/await 的价值,不只是减少缩进,更重要的是让异步流程和错误处理都更可控。

8. 面试高频速记

1. async 函数返回什么?

返回 Promise。

2. await 123 会发生什么?

等价于 await Promise.resolve(123),最终拿到 123

3. new Promise(() => {}) 里的代码什么时候执行?

立即同步执行。

4. Promise.thensetTimeout(..., 0) 谁先执行?

同一轮里通常 Promise.then 先执行,因为它是微任务,setTimeout 是宏任务。

5. Promise.all 里有一个失败会怎样?

聚合 Promise 立即 rejected,但其他已经开始的任务不会自动取消。

6. Promise.allPromise.allSettled 的区别?

all 要求全部成功;allSettled 只要求全部结束,不管成败。

7. await 会阻塞线程吗?

不会,只会暂停当前 async 函数。

8. 怎么把多个无依赖请求并发起来?

先同时发起,再 await Promise.all([...])

9. async/await 能完全替代 Promise 吗?

不能。async/await 底层还是 Promise,而且像并发组合、批量控制时,Promise 的静态方法依然非常重要。

10. 事件循环一句话怎么答?

执行一个宏任务,清空所有微任务,然后再执行下一个宏任务。

9. 一句话总结

  • 回调函数是基础,Promise 是统一抽象,async/await 是主流写法。
  • Promise 重点掌握状态、链式调用、错误传递和并发 API。
  • async/await 重点掌握“本质是 Promise 语法糖”和“避免把并发写成串行”。
  • 事件循环重点掌握“宏任务/微任务”和执行顺序。
  • 面试题里最容易丢分的点:Promise 构造器同步执行、await 后续是微任务、setTimeout(fn, 0) 不会立刻执行。

评论区

欢迎留言、补充或勘误。

xiaoba.blog