JS异步编程

JS异步编程

_

一,同步和异步

1.1 生活类比

同步(Synchronous):你去食堂打饭,排队时你死盯着窗口,前面的人不端走,你就不动,也不玩手机。你“阻塞”了。

异步(Asynchronous):你去食堂打饭,拿了个叫号振动器。排队时你回座位刷手机,振动器响了再去取餐。你“非阻塞”,系统效率更高。

1.2 代码demo

同步代码

console.log('1. 开始做饭');

function cookRice() {
  // 假设煮饭需要 3 秒(这里用循环模拟耗时,会卡住)
  let start = Date.now();
  while (Date.now() - start < 3000) {} // 死循环 3 秒
  console.log('2. 饭煮好了');
}

cookRice();
console.log('3. 可以炒菜了');

输出顺序(铁定不变):

1. 开始做饭
(卡顿 3 秒)
2. 饭煮好了
3. 可以炒菜了

第 3 行必须等煮饭完成才能执行,页面会卡死。

异步代码

console.log('1. 开始做饭');

setTimeout(() => {
  console.log('2. 饭煮好了(异步通知)');
}, 3000); // 3秒后执行

console.log('3. 可以炒菜了(不用等饭)');

输出顺序

1. 开始做饭
3. 可以炒菜了(不用等饭)
(约 3 秒后)
2. 饭煮好了(异步通知)

第 3 行不等煮饭,先炒菜,饭好了再回来处理。

1.3 核心结论

维度同步 (Sync)异步 (Async)
执行顺序从上到下,一行等一行耗时任务先“挂起”,先执行后面的代码
是否阻塞阻塞(卡住后面的代码)非阻塞(后面的代码照常运行)
返回值直接拿到结果通过回调/Promise 等“将来”拿结果
典型场景普通计算、数组遍历网络请求、定时器、文件读取、事件监听
对用户影响页面卡顿(白屏)页面流畅(体验好)

二,回调函数

2.1 回调函数到底是什么

定义:把一个函数作为参数传给另一个函数,在未来某个时刻被调用执行。

生活类比:你去奶茶店点单,店员给你一个号码牌说:“等叫号的时候,你凭牌来取”。

  • 这个“取奶茶的动作”就是回调函数,叫号的时候才会执行。

2.2 回调的两种常见形态

同步回调

function greet(name, callback) {
  callback(name);  // 立即调用
}

greet('小明', (name) => {
  console.log(`你好,${name}`);
});
// 输出:你好,小明(立即)

这个只是函数传参,不涉及异步

异步回调

console.log('1. 开始点单');

// setTimeout 就是异步回调的典型
setTimeout(() => {
  console.log('2. 奶茶做好了(回调执行)');
}, 2000);

console.log('3. 先去看会儿手机');

输出:

1. 开始点单
3. 先去看会儿手机
(2秒后)
2. 奶茶做好了(回调执行)

() => { ... } 这个箭头函数就是回调函数,它被放在未来(2 秒后)执行。

2.3 常见的异步回调场景

场景示例代码
定时器setTimeout(() => {}, 1000)
DOM 事件btn.addEventListener('click', () => {})
Node.js 读文件fs.readFile('a.txt', (err, data) => {})
旧式 Ajax$.get('/api', (res) => {})

2.4 回调地狱

当你有多个异步任务必须按顺序执行(比如:先读 A 文件 → 再读 B 文件 → 再提交数据),回调就会层层嵌套,变成“三角形代码”。

回调地狱现场

// 伪代码:先登录 → 获取用户信息 → 获取订单 → 获取订单详情
login('user', 'pass', (err, token) => {
  if (err) throw err;
  getUserInfo(token, (err, user) => {
    if (err) throw err;
    getOrders(user.id, (err, orders) => {
      if (err) throw err;
      getOrderDetail(orders[0].id, (err, detail) => {
        if (err) throw err;
        console.log('最终详情:', detail);
        // 如果再嵌套一层,就变成“金字塔”了 
      });
    });
  });
});

回调地狱的 4 大问题

  1. 代码横向膨胀,可读性极差(像圣诞树)。
  2. 错误处理重复,每个层级都要 if (err)。
  3. 难以维护,改一个逻辑要动多层。
  4. 无法直接使用 return / try-catch,控制流被割裂。

三,事件循环

3.1 JS 为什么需要事件循环

核心前提:

  • JS 是单线程(只有一个主线程干活)。
  • 但浏览器/Node 环境提供了 Web APIs(定时器、HTTP请求、DOM事件)来帮我们做耗时任务。
  • 当这些“帮手”完成任务后,需要把回调函数排好队,等主线程空闲了再执行。
  • 事件循环(Event Loop) 就是那个不停看“主线程是否空闲”的调度员。

生活类比:地铁调度

把 JS 运行机制想象成一个地铁站:

概念类比
主线程(调用栈)地铁进站口,一次只能通过一个人
宏任务队列普通乘客通道(排队等进站)
微任务队列VIP 快速通道(永远插队到普通通道前面)
事件循环地铁调度员,不停检查进站口有没有人,没人就从队列拉人进来

规则:

  1. 先执行完当前所有的同步代码(清空进站口)。
  2. 然后清空整个微任务队列(VIP 全放行)。
  3. 最后只取一个宏任务来执行(放一个普通乘客)。
  4. 执行这个宏任务时,如果又产生了新的微任务,继续清空微任务。
  5. 重复以上循环。

先同步,再微任务,最后宏任务(一次只取一个),微任务清空后,才取下一个宏任务

3.2 宏任务 vs 微任务

类型包含哪些 API
宏任务(MacroTask)setTimeout、setInterval、setImmediate、I/O 操作、UI 渲染
微任务(MicroTask)Promise.then()、Promise.catch()、MutationObserver、process.nextTick(Node)

注意:Promise 本身是同步的,但 then/catch/finally 里的回调是微任务。

四,定时器

4.1 setTimeout

setTimeout 不保证精确延迟,它只保证“至少等待指定时间后执行”。

也就是说:你写 setTimeout(fn, 1000),意思是 “最早 1 秒后执行”,而不是 “1 秒整执行”。

4.2 为什么不精准?

主线程被阻塞 是罪魁祸首。

代码演示

console.log('1. 设置定时器,延迟 100ms');

setTimeout(() => {
  console.log('2. 定时器回调执行了');
}, 100);

// 模拟一个耗时 500ms 的同步任务(阻塞主线程)
let start = Date.now();
while (Date.now() - start < 500) {
  // 死循环,啥也不干
}

console.log('3. 耗时任务结束');

实际输出顺序:

1. 设置定时器,延迟 100ms
3. 耗时任务结束   (约 500ms 后输出)
2. 定时器回调执行了  (紧接着输出)

发生了什么?

  1. 第 1 行:setTimeout 注册,计时器在后台开始倒计时(100ms)。
  2. 第 100ms 时:定时器到期,回调被放入宏任务队列等待执行。
  3. 但此时主线程正在执行那个 while 循环(还没结束!),所以回调在队列里干等。
  4. 第 500ms 时:while 结束,主线程空闲。
  5. 主线程检查宏任务队列,拿出定时器回调执行。

结果:明明写 100ms,实际却等了 500ms+。

4.3 最小延迟限制

即使你写 setTimeout(fn, 0),浏览器也不会真的 0ms 执行。

规范规定:

  • 浏览器环境:嵌套层级 ≥ 5 时,最小延迟为 4ms
  • Node.js 环境:最小延迟为 1ms

演示代码

let start = Date.now();

function nestedTimeout() {
  if (Date.now() - start > 50) {
    console.log('实际耗时:', Date.now() - start, 'ms');
    return;
  }
  setTimeout(nestedTimeout, 0);
}

nestedTimeout();

你会发现实际耗时远大于 0ms,因为浏览器强制插入了 4ms 的“节流”。

4.4 两个替代方案

方案代码特点
queueMicrotaskqueueMicrotask(() => console.log('尽快'))微任务,当前宏任务结束后立即执行
Promise.resolve().thenPromise.resolve().then(() => console.log('尽快'))同样是微任务,用法更常见

对比演示

console.log('1');

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

queueMicrotask(() => console.log('3 (微任务)'));

Promise.resolve().then(() => console.log('4 (微任务)'));

console.log('5');

输出:

1
5
3
4
2 (setTimeout)

queuMicrotask 和 Promise.then 都在宏任务之前执行,真正做到了“尽快”。

4.5 实际开发中的痛点场景

场景 1:动画卡顿

//  错误:用 setTimeout 做动画(不准,会掉帧)
function animate() {
  moveBox();
  setTimeout(animate, 16); // 想 60fps,但不准
}

// 正确:用 requestAnimationFrame(由浏览器刷新率驱动)
function animate() {
  moveBox();
  requestAnimationFrame(animate);
}

场景 2:倒计时不准

// 倒计时会越走越慢
let count = 10;
setInterval(() => {
  count--;
  console.log(count);
}, 1000);
// 如果用户切到后台,浏览器会降低定时器频率,倒计时会滞后

// 用时间戳修正
let targetTime = Date.now() + 10 * 1000;
function updateCountdown() {
  let remaining = targetTime - Date.now();
  if (remaining <= 0) return;
  console.log(Math.ceil(remaining / 1000));
  requestAnimationFrame(updateCountdown);
}

五,Promise

5.1 什么是Promise?

Promise 就像“外卖订单”:

外卖订单状态Promise 状态含义
已下单,等骑手接单Pending(进行中)异步任务还在执行,结果未知
骑手已取餐,正在配送Fulfilled(已完成)异步任务成功,拿到了结果(你的外卖)
商家爆单,订单取消Rejected(已拒绝)异步任务失败,得到了错误原因

关键特性:

  • 状态只能改变一次:要么 Pending → Fulfilled,要么 Pending → Rejected。
  • 一旦状态确定,永远不变(不可逆)。
  • 你可以在任何时候(状态确定之前或之后)去“获取”结果。

5.2 Promise 的基本用法

1. 创建一个 Promise

const promise = new Promise((resolve, reject) => {
  // 这里放异步任务(比如定时器、网络请求)
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('外卖送到了!');  // 成功 → 状态变为 Fulfilled
    } else {
      reject('商家取消订单了 '); // 失败 → 状态变为 Rejected
    }
  }, 1000);
});

解读:

  • new Promise 构造函数接收一个执行器函数(executor),它立即执行(同步)。
  • 执行器函数有两个参数:resolve 和 reject,它们是 JS 引擎提供的函数。
  • 调用 resolve(value) → 状态变为 Fulfilled,value 就是结果。
  • 调用 reject(reason) → 状态变为 Rejected,reason 就是错误原因。

2. 消费 Promise

promise
  .then((result) => {
    console.log('成功:', result);
  })
  .catch((error) => {
    console.log('失败:', error);
  })
  .finally(() => {
    console.log('无论成功失败,都会执行');
  });

输出(1 秒后):

成功: 外卖送到了!
无论成功失败,都会执行

3. 链式调用

then() 方法返回一个新的 Promise,这是链式调用的基础。

const promise = new Promise((resolve) => {
  resolve(1);
});

promise
  .then((value) => 
    console.log('第一次:', value);  // 1
    return value + 1;               // 返回 2(会被包装成 Promise)
  })
  .then((value) => {
    console.log('第二次:', value);  // 2
    return value + 1;
  })
  .then((value) => {
    console.log('第三次:', value);  // 3
  });

输出:

第一次: 1
第二次: 2
第三次: 3

核心规则:

  • 在 then 中 return 一个普通值 → 会被自动包装成 Promise.resolve(值)
  • 在 then 中 return 一个 Promise → 会等待这个 Promise 完成
  • 在 then 中 抛出一个错误(throw) → 会触发最近的 .catch()

4. 错误处理

// 写法一:用 .catch(推荐)
promise
  .then((data) => console.log(data))
  .catch((err) => console.error(err));

// 写法二:then 的第二个参数
promise
  .then(
    (data) => console.log(data),
    (err) => console.error(err)
  );

区别:

  • .catch 能捕获前面所有 then 中抛出的错误。
  • then 的第二个参数只能捕获当前 Promise 的错误。

5.3 Promise 的静态方法

方法作用
Promise.resolve(value)直接返回一个成功状态的 Promise
Promise.reject(reason)直接返回一个失败状态的 Promise
Promise.all([p1, p2])全部成功才成功,任何一个失败则立即失败
Promise.race([p1, p2])谁先完成(成功或失败)就返回谁

代码示例

// Promise.resolve 快速创建成功 Promise
Promise.resolve('直接成功')
  .then(console.log);  // 直接成功

// Promise.all 并发执行
Promise.all([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]).then((results) => {
  console.log(results);  // [1, 2, 3]
});

// 如果有一个失败
Promise.all([
  Promise.resolve(1),
  Promise.reject('出错了'),
  Promise.resolve(3)
]).catch((err) => {
  console.log(err);  // 出错了(立即失败,不会等第三个)
});

5.4 Promise 解决了什么?

回调的痛点Promise 的解决方案
回调地狱(嵌套金字塔)链式调用(.then().then())
错误处理分散(每层 if(err))统一错误捕获(.catch())
控制流割裂(不能 return)可以 return 值,流程更自然
难以组合多个异步Promise.all / race 轻松并发

对比代码

// 回调地狱
login('user', 'pass', (err, token) => {
  if (err) throw err;
  getUserInfo(token, (err, user) => {
    if (err) throw err;
    getOrders(user.id, (err, orders) => {
      if (err) throw err;
      console.log(orders);
    });
  });
});

//  Promise 链式(扁平化)
login('user', 'pass')
  .then(token => getUserInfo(token))
  .then(user => getOrders(user.id))
  .then(orders => console.log(orders))
  .catch(err => console.error(err));

六,async/await


6.1 async/await是什么

async/await 是 Promise 的语法糖,它让异步代码看起来像同步代码,底层依然是 Promise + 微任务。

本质:

  • async 函数永远返回一个 Promise
  • await 会暂停函数执行,等待 Promise 状态确定后继续
  • await 只能在 async 函数中使用

6.2 生活类比

回调 /Promise 方式:你点完奶茶,拿到号牌(Promise),然后可以到处逛,等振动器响了再回来取(.then)。

async/await 方式:你点完奶茶,站在柜台前等着(await),店员做好直接递给你,你拿了就走。代码看起来像 "排队等着",但实际上并没有阻塞主线程(JS 引擎帮你做了 "暂停和恢复")。

6.3 基础语法

对比 1:单个异步任务

Promise 写法:

function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('小明'), 1000);
  });
}

fetchUser().then((user) => {
  console.log(user);  // 小明
});

async/await 写法:

function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('小明'), 1000);
  });
}

async function getUser() {
  const user = await fetchUser();  // 等待 1 秒
  console.log(user);  // 小明
}

getUser();

关键区别:

  • await 让代码看起来像"同步等待",但不会阻塞主线程
  • await 后面的代码被隐式包装成 .then() 回调

对比 2:多个顺序依赖的任务

Promise 链式:

function login() { return Promise.resolve('token'); }
function getUser(token) { return Promise.resolve({ id: 1, name: '小明' }); }
function getOrders(userId) { return Promise.resolve(['订单1', '订单2']); }

login()
  .then(token => getUser(token))
  .then(user => getOrders(user.id))
  .then(orders => console.log(orders))
  .catch(err => console.error(err));

async/await 写法(完胜):

async function getOrderList() {
  try {
    const token = await login();
    const user = await getUser(token);
    const orders = await getOrders(user.id);
    console.log(orders);
  } catch (err) {
    console.error(err);
  }
}

getOrderList();

逻辑顺序从上到下,一目了然,没有任何嵌套!

6.4 核心机制拆解

看这段代码:

async function demo() {
  console.log('1');
  const result = await someAsyncTask();
  console.log('2');
}
demo();
console.log('3');

实际执行顺序:

1
3
(等待 someAsyncTask 完成)
2

底层发生了什么?(重要!)

// 上面的 async/await 等价于:
function demo() {
  console.log('1');
  return someAsyncTask().then((result) => {
    console.log('2');
  });
}
demo();
console.log('3');

关键结论:

  1. await 后面的代码,相当于被放进了 .then() 回调里。
  2. await 会让出主线程,所以后面的同步代码(console.log('3'))会先执行。
  3. async 函数本身会立即执行到第一个 await 处,然后返回一个 Pending 状态的 Promise。

6.5 错误处理

方式一:try-catch

async function fetchData() {
  try {
    const data = await fetch('/api/data');
    const json = await data.json();
    console.log(json);
  } catch (err) {
    console.error('请求失败:', err.message);
  }
}

方式二:.catch

async function fetchData() {
  const data = await fetch('/api/data').catch(err => {
    console.error('请求失败:', err);
    return null;  // 返回默认值
  });
  if (data) {
    const json = await data.json();
    console.log(json);
  }
}

核心规则:

  • await 后面 Promise 被 reject 了,会抛出异常,可以用 try-catch 捕获。
  • 如果多个 await,一个 try-catch 就能捕获所有错误。
  • 如果不想整个函数中断,可以在单个 await 后面加 .catch()。

6.6 关键注意点

坑 1:忘记加 await

//  错误:没有 await
async function getData() {
  const result = fetch('/api');  // result 是 Promise,不是数据
  console.log(result);  // Promise { <pending> }
}

// 正确
async function getData() {
  const result = await fetch('/api');
  console.log(result);  // 实际数据
}

坑 2:在顶层作用域使用 await(ES2022 已支持)

旧方式(需要包装):

// 旧语法:顶层不能用 await
const data = await fetch('/api');  // SyntaxError

// 必须包装在 async 函数里
(async () => {
  const data = await fetch('/api');
  console.log(data);
})();

新方式(ES2022 模块顶层 await):

//  在 ES Module 中可以直接用(需 type="module")
const data = await fetch('/api');
console.log(data);

坑 3:并发错误(用 Promise.all 优化)

//  慢:顺序执行,总耗时 2 秒
async function getData() {
  const user = await fetchUser();     // 等待 1 秒
  const orders = await fetchOrders(); // 再等待 1 秒
  return { user, orders };
}

// 快:并发执行,总耗时 1 秒
async function getData() {
  const [user, orders] = await Promise.all([
    fetchUser(),
    fetchOrders()
  ]);
  return { user, orders };
}

原则:没有依赖关系的异步任务,用 Promise.all 并发执行!

七,异常处理

7.1 结论

在 async/await 中用 try-catch,在 Promise 链中用 .catch()。
两种方式核心原理一样,但使用场景和细节不同。

7.2 两种方式的本质对比

维度try-catch.catch()
适用场景async/await 写法Promise 链式写法
捕获范围捕获 await 抛出的错误捕获链中前面所有 then 的错误
是否阻塞会中断 try 块后续代码会跳过错误后的 then,跳到 catch
错误恢复在 catch 块中处理,然后继续catch 返回新 Promise,后续 then 继续
可读性更接近同步代码链式风格

7.3 场景 1:async/await 中的错误处理

标准写法(推荐)

async function fetchUserData() {
  try {
    const response = await fetch('/api/user');
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const data = await response.json();
    console.log('用户数据:', data);
    return data;
  } catch (err) {
    console.error('获取用户数据失败:', err.message);
    // 错误恢复:返回一个默认值
    return { name: '默认用户', id: -1 };
  }
}

// 调用方
const user = await fetchUserData();
console.log('最终结果:', user);

关键点:

  • try 块中任何一个 await 出错,都会跳到 catch。
  • catch 中可以返回默认值、重试、或者重新抛出错误。
  • 错误恢复后,函数正常返回,调用方不会收到异常。

单个 await 独立错误处理(细粒度控制)

async function complexTask() {
  // 任务 A 失败不影响任务 B
  let user = null;
  try {
    user = await fetchUser();
  } catch (err) {
    console.warn('获取用户失败,使用默认用户');
    user = { name: '游客' };
  }

  // 任务 B 失败则整体失败
  const orders = await fetchOrders(user.id);
  return { user, orders };
}

重新抛出错误(向上传递)

async function login() {
  try {
    const result = await doLogin();
    return result;
  } catch (err) {
    // 记录日志后,重新抛出
    console.error('登录失败,已记录日志');
    throw new Error('登录失败,请稍后重试');
  }
}

// 调用方必须处理这个错误
try {
  await login();
} catch (err) {
  showToast(err.message);
}

7.4 场景 2:Promise 链中的错误处理

标准写法

fetch('/api/user')
  .then(response => {
    if (!response.ok) throw new Error('请求失败');
    return response.json();
  })
  .then(data => {
    console.log('用户数据:', data);
    return data;
  })
  .catch(err => {
    console.error('错误:', err.message);
    return { name: '默认用户' };  // 错误恢复
  });

关键点:

  • .catch() 捕获前面所有 .then() 中抛出的错误。
  • .catch() 返回一个新 Promise,后续的 .then() 会继续执行。

多个独立任务的错误隔离

// 错误:一个失败全部失败
Promise.all([
  fetchUser(),
  fetchOrders(),
  fetchSettings()
]).catch(err => {
  console.error('某个任务失败:', err);
});

// 正确:每个任务独立处理
Promise.all([
  fetchUser().catch(err => ({ name: '默认用户' })),
  fetchOrders().catch(err => []),
  fetchSettings().catch(err => ({ theme: 'light' }))
]).then(([user, orders, settings]) => {
  console.log(user, orders, settings);
});

7.5 try-catch 无法捕获什么?

1. 未 await 的 Promise 错误

//  错误:没有 await,try-catch 捕获不到
async function bad() {
  try {
    Promise.reject('出错了');  // 没有 await,异步执行
  } catch (err) {
    console.log('捕获不到!');  // 不会执行
  }
}
// 会触发 UnhandledPromiseRejection

// 正确:必须 await
async function good() {
  try {
    await Promise.reject('出错了');
  } catch (err) {
    console.log('捕获到了:', err);  // 捕获到了
  }
}

2. 异步回调中的错误

// 错误:setTimeout 回调中的错误捕获不到
async function bad() {
  try {
    setTimeout(() => {
      throw new Error('回调中的错误');
    }, 0);
  } catch (err) {
    console.log('捕获不到!');  // 不会执行
  }
}

// 正确:用 Promise 包装
function delayError() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('回调中的错误'));
    }, 0);
  });
}

async function good() {
  try {
    await delayError();
  } catch (err) {
    console.log('捕获到了:', err.message);
  }
}

7.6 全局错误兜底

浏览器环境

// 捕获未处理的 Promise 错误
window.addEventListener('unhandledrejection', (event) => {
  console.error('未处理的 Promise 错误:', event.reason);
  // 可以上报到监控系统
  reportError(event.reason);
});

// 捕获未处理的同步错误
window.addEventListener('error', (event) => {
  console.error('未处理的错误:', event.message);
});

Node.js 环境

// 捕获未处理的 Promise 错误
process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的 Promise 错误:', reason);
});

// 捕获未处理的同步错误
process.on('uncaughtException', (err) => {
  console.error('未捕获的异常:', err);
  // 注意:不要在这里重启进程,而是记录后退出
  process.exit(1);
});

7.7 实战最佳实践

#实践说明
1永远给 await 加上 try-catch尤其是网络请求、文件操作
2catch 后要返回合理默认值避免整个流程中断
3需要上层处理的错误要 throw不要吞掉错误
4并发任务用 Promise.all + 独立 .catch避免一个失败全盘崩溃
5添加全局 unhandledrejection 兜底作为最后防线

八,并发控制

8.1 总览对比

方法返回条件失败行为适用场景
Promise.all全部成功一个失败立即失败所有任务必须全部成功
Promise.race任意一个完成失败算完成超时控制、竞速
Promise.allSettled全部完成(无论成败)不会失败需要所有结果,允许部分失败
Promise.any任意一个成功全部失败才失败多个备用源,取最快成功的

8.2 逐个拆解

Promise.all

场景:加载用户资料、订单列表、权限配置,缺一个页面都不能展示。

function fetchUser() {
  return new Promise(resolve => setTimeout(() => resolve('用户数据'), 500));
}
function fetchOrders() {
  return new Promise(resolve => setTimeout(() => resolve('订单数据'), 800));
}
function fetchSettings() {
  return new Promise(resolve => setTimeout(() => resolve('设置数据'), 300));
}

// 并发执行,总耗时 = 最慢的那个(800ms),而不是 500+800+300=1600ms
async function loadDashboard() {
  try {
    const [user, orders, settings] = await Promise.all([
      fetchUser(),
      fetchOrders(),
      fetchSettings()
    ]);
    console.log('全部加载完成:', { user, orders, settings });
    // 渲染页面...
  } catch (err) {
    console.error('某个数据加载失败,整个页面无法展示');
    showErrorPage();
  }
}

关键特性:

  • 输入数组任何一个被 reject,Promise.all 立即 reject。
  • 结果顺序保持与输入一致(不管谁先完成)。
  • 总耗时 = 最慢那个任务的时间。

Promise.race

场景:超时控制、多个 CDN 地址抢最快加载。

场景一:超时控制(最常用)

function fetchWithTimeout(url, timeout = 3000) {
  return Promise.race([
    fetch(url),  // 正常请求
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('请求超时')), timeout)
    )
  ]);
}

// 使用
try {
  const response = await fetchWithTimeout('/api/slow', 2000);
  const data = await response.json();
  console.log(data);
} catch (err) {
  console.log('请求失败或超时:', err.message);
}

场景二:多个数据源择优

function queryCDN(cdnName, delay) {
  return new Promise(resolve => {
    setTimeout(() => resolve(`${cdnName} 返回数据`), delay);
  });
}

const result = await Promise.race([
  queryCDN('阿里云', 1000),
  queryCDN('腾讯云', 500),   // 这个最快
  queryCDN('华为云', 1500)
]);
console.log(result);  // "腾讯云 返回数据"

关键特性:

  • 输入数组任意一个状态改变(成功或失败),race 立即返回。
  • 其他任务不会取消,只是被忽略(会继续执行但结果被丢弃)。
  • 常用于超时保护。

Promise.allSettled

场景:批量上传文件,失败的不影响成功;批量埋点,部分失败不阻塞主流程。

const uploadTasks = [
  uploadFile('image1.jpg'),
  uploadFile('image2.jpg'),
  uploadFile('image3.jpg'),  // 这个可能失败
];

const results = await Promise.allSettled(uploadTasks);

// 结果格式
results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`文件 ${index + 1} 上传成功:`, result.value);
  } else {
    console.log(`文件 ${index + 1} 上传失败:`, result.reason);
  }
});

// 筛选出成功和失败
const successful = results.filter(r => r.status === 'fulfilled').map(r => r.value);
const failed = results.filter(r => r.status === 'rejected').map(r => r.reason);

返回值格式:

// 成功:{ status: 'fulfilled', value: '结果' }
// 失败:{ status: 'rejected', reason: '错误' }

关键特性:

  • 永远不会 reject,永远 resolve。
  • 适合"部分成功可接受"的场景。
  • ES2020 引入,旧环境需要 polyfill。

Promise.any

场景:多个备用服务器,哪个能用用哪个;多个广告接口,取最快成功的。

function requestServer(serverName, shouldFail = false) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(`${serverName} 不可用`);
      } else {
        resolve(`${serverName} 响应成功`);
      }
    }, Math.random() * 1000);
  });
}

try {
  const result = await Promise.any([
    requestServer('服务器A', false),
    requestServer('服务器B', true),   // 失败
    requestServer('服务器C', true),   // 失败
  ]);
  console.log('最快可用的服务器:', result);
} catch (err) {
  // 只有全部失败才会进入这里
  console.log('所有服务器都不可用');
}

关键特性:

  • 只要一个成功就 resolve。
  • 全部失败才 reject,错误是一个 AggregateError(包含所有错误)。
  • ES2021 引入,较新特性。

8.3 实战对比

场景:请求三个不同的推荐数据源。

const source1 = fetch('/api/recommend/1').catch(() => null);
const source2 = fetch('/api/recommend/2').catch(() => null);
const source3 = fetch('/api/recommend/3').catch(() => null);
需求最佳方案
三个都必须成功才能展示Promise.all([source1, source2, source3])
任意一个成功就展示Promise.any([source1, source2, source3])
全部展示,失败的显示兜底Promise.allSettled([source1, source2, source3])
2秒内哪个先返回用哪个Promise.race([Promise.all(...), timeout])

8.4 常见陷阱

陷阱 1:Promise.all 中一个失败不会等其他人

Promise.all([
  Promise.resolve(1),
  Promise.reject('错误'),
  Promise.resolve(3)  // 这个不会执行完,因为上面已经失败了
]).catch(err => console.log(err));  // "错误"

陷阱 2:race 不取消其他任务(内存泄漏风险)

const result = await Promise.race([
  fetch('/api/slow'),      // 这个请求如果很慢,即使 race 返回了
  new Promise(resolve => setTimeout(() => resolve('超时'), 1000))
]);
// 慢请求依然在后台执行,只是结果被丢弃
// 实际项目中需要用 AbortController 取消

陷阱 3:any 和 race 的混淆

// race:第一个状态改变就返回(可能是失败)
// any:第一个成功的返回;全部失败才失败

Promise.race([
  Promise.reject('A'),
  Promise.resolve('B')
]);  // 返回 'B'(因为 B 先成功)

Promise.any([
  Promise.reject('A'),
  Promise.resolve('B')
]);  // 返回 'B'

8.5 总结

#场景推荐方案
1页面初始化加载多个模块Promise.all + 全局 Loading
2请求超时控制Promise.race + setTimeout
3批量操作(部分失败可接受)Promise.allSettled
4多备用服务器/数据源容灾Promise.any
5大量并发任务(几十上百个)用 p-limit 或 p-lazy 控制并发数

好!你直接点到了进阶核心,我一次性把这两个讲透。

九,手写 Promise

9.1 先搭骨架

一个 Promise 的核心就三样东西:

  1. 状态(pending / fulfilled / rejected)
  2. 值(成功结果或失败原因)
  3. then 方法(注册回调)
class MyPromise {
  constructor(executor) {
    this.state = 'pending';      // 初始状态
    this.value = undefined;      // 存放结果
    this.reason = undefined;     // 存放失败原因
    this.onFulfilledCallbacks = [];  // 成功回调队列
    this.onRejectedCallbacks = [];   // 失败回调队列

    // resolve 函数
    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        // 执行所有注册的成功回调
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };

    // reject 函数
    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  then(onFulfilled, onRejected) {
    // 处理值穿透:如果没传回调,默认传递值
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

    // 关键:then 必须返回一个新的 Promise
    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        // 用微任务模拟(实际规范是微任务,这里用 setTimeout 模拟)
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolve(x);
          } catch (err) {
            reject(err);
          }
        });
      }

      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolve(x);
          } catch (err) {
            reject(err);
          }
        });
      }

      if (this.state === 'pending') {
        // 还没完成,先把回调存起来
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              resolve(x);
            } catch (err) {
              reject(err);
            }
          });
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolve(x);
            } catch (err) {
              reject(err);
            }
          });
        });
      }
    });

    return promise2;
  }

  // 静态方法:快速创建成功/失败 Promise
  static resolve(value) {
    return new MyPromise(resolve => resolve(value));
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
}

9.2 测试

const p = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('成功!');
  }, 1000);
});

p.then((value) => {
  console.log('结果:', value);
  return '链式传递';
}).then((value) => {
  console.log('链式:', value);
});

// 输出(1秒后):
// 结果: 成功!
// 链式: 链式传递

9.3 核心难点

上面代码有个问题:如果 onFulfilled 返回一个 MyPromise,没有处理 "等待它完成" 的逻辑。

完整版需要实现 resolvePromise 函数(面试常考):

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('循环引用'));
  }

  if (x instanceof MyPromise) {
    // 如果 x 是 Promise,等待它的结果
    x.then(
      (value) => resolve(value),
      (err) => reject(err)
    );
  } else {
    // 普通值直接 resolve
    resolve(x);
  }
}

然后在你 then 里的 resolve(x) 改成 resolvePromise(promise2, x, resolve, reject)。

十,异步迭代器与 for-await-of

当你处理流式数据(比如分页请求、WebSocket 消息流、逐行读取大文件)时,普通 for...of 不够用,需要 for-await-of。

10.1 同步迭代器 vs 异步迭代器

类型协议方法使用方式
同步迭代器Iterablenext() 返回 { value, done }for...of
异步迭代器AsyncIterablenext() 返回 Promise<{ value, done }>for-await-of

10.2 异步生成器

// 定义一个异步生成器
async function* asyncNumberGenerator() {
  for (let i = 1; i <= 3; i++) {
    await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟异步
    yield i;  // 每秒吐出一个数字
  }
}

// 使用 for-await-of 消费
(async () => {
  console.log('开始');
  for await (const num of asyncNumberGenerator()) {
    console.log('收到:', num);
  }
  console.log('结束');
})();

// 输出(每隔 1 秒输出一行):
// 开始
// 收到: 1
// 收到: 2
// 收到: 3
// 结束

10.3 实际场景

// 模拟分页 API:每页 2 条
async function fetchPage(page) {
  const data = [
    { id: 1, name: 'A' },
    { id: 2, name: 'B' },
    { id: 3, name: 'C' },
    { id: 4, name: 'D' },
    { id: 5, name: 'E' },
  ];
  const start = (page - 1) * 2;
  const end = start + 2;
  await new Promise(r => setTimeout(r, 500)); // 模拟网络延迟
  return {
    items: data.slice(start, end),
    hasMore: end < data.length,
  };
}

// 异步生成器:逐页获取,直到没有更多数据
async function* paginate() {
  let page = 1;
  let hasMore = true;
  while (hasMore) {
    const result = await fetchPage(page);
    yield* result.items;  // yield* 逐个吐出 items 中的元素
    hasMore = result.hasMore;
    page++;
  }
}

// 消费:逐条处理
(async () => {
  for await (const item of paginate()) {
    console.log('处理用户:', item.name);
    // 可以在这里逐个处理,不需要等所有数据加载完
  }
})();

// 输出:
// 处理用户: A
// 处理用户: B  (等待 500ms)
// 处理用户: C
// 处理用户: D  (等待 500ms)
// 处理用户: E

10.4 对比

不用迭代器(一次性加载所有数据):

const allItems = [];
let page = 1;
let hasMore = true;
while (hasMore) {
  const result = await fetchPage(page);
  allItems.push(...result.items);
  hasMore = result.hasMore;
  page++;
}
// 所有数据加载完才能开始处理
allItems.forEach(item => console.log(item));
// 缺点:内存占用大,处理延迟高

用异步迭代器(边加载边处理):

for await (const item of paginate()) {
  console.log(item);  // 来一条处理一条
}
// 优点:内存友好,响应快

10.5 把普通异步函数变成可迭代

// 将事件流(如 WebSocket)包装成异步迭代器
function createAsyncIterable(ws) {
  return {
    [Symbol.asyncIterator]() {
      return {
        next() {
          return new Promise((resolve) => {
            ws.onmessage = (event) => {
              resolve({ value: event.data, done: false });
            };
            ws.onclose = () => {
              resolve({ value: undefined, done: true });
            };
          });
        }
      };
    }
  };
}

// 使用
const ws = new WebSocket('wss://example.com');
for await (const message of createAsyncIterable(ws)) {
  console.log('收到消息:', message);
}

问:for-await-of 和 for...of 有什么区别?

维度for...offor-await-of
适用对象同步可迭代对象异步可迭代对象
next() 返回值直接返回 { value, done }返回 Promise<{ value, done }>
使用时任何地方直接使用必须在 async 函数内
底层同步执行每次迭代都会 await

10.6 什么时候用异步迭代器?

场景传统方案异步迭代器方案
分页加载大量数据while + await,一次性 push 到数组for-await-of 逐条处理
WebSocket 消息流用 onmessage 回调包装成迭代器,更优雅
大文件逐行读取回调或 stream 事件for-await-of 逐行处理
定时轮询setInterval异步生成器 + for-await-of
JS进阶 2026-07-21
Ajax入门 2026-08-24

© 2026 日志记录