Skip to content

Promise 基本语法与Promise 实例的方法(Day17 2/3) ​

本页对应 Day17-Promise,共 6 段视频,约 1 小时 15 分。

学习目标 ​

  • 能对普通函数、函数表达式、箭头函数、立即执行函数及对象方法正确添加 async 关键字,并判断其是否构成合法的 async 函数。
  • 能说明 async 函数始终返回 Promise 对象,并根据函数内 return 值或抛出异常,判断返回 Promise 的状态与结果。
  • 能编写包含一个或多个 await 的 async 函数,说明 await 表达式的取值规则,并处理右侧 Promise 为 reject 时的异常。
  • 能将基于 then 的链式调用改写为 async/await 形式,并保证异步步骤按预期顺序执行。
  • 能使用 async/await 配合循环改写 Promise.all 场景,正确收集异步结果并说明其与并行请求的差异。
  • 能区分微队列、交互队列和延迟队列,分析包含定时器、DOM 事件、Ajax 与 Promise 回调的代码输出顺序。

知识点 ​

Promise 基本语法:先会创建、改状态、再取结果 ​

转写稿里反复强调:async/await 只是 Promise 的“语法糖”,底层仍然是 Promise 的状态管理。也就是说,后面想用 await 代替 then 的链式调用、避免回调地狱,前提是先把本节三件事学清楚:怎么创建 Promise、怎么改状态、怎么设置回调拿结果。下面按“为什么需要 → 怎么想 → 怎么做 → 易错点”展开。

① 使用 Promise 构造函数创建 promise 对象 ​

为什么需要:异步任务的结果不会立刻回来。我们需要一个对象来“占位”,将来成功或失败时再通知回调。Promise 构造函数就是用来创建这个占位对象的。

怎么想:实例化 Promise 时,必须传一个回调函数作为参数,这个回调叫执行器函数。它不是等你调用 then 才执行,而是在 new Promise(...) 时自动、同步执行。执行器会收到两个参数,且两个参数都是函数:第一个通常叫 resolve,第二个通常叫 reject。

怎么做:

js
 new Promise((res, rej) => {
     console.log('第一个参数:', res);
     console.log('第二个参数:', rej);
 });

运行后可以看到 res、rej 都是函数。注意:这里只是创建 Promise,还没有用 then 取结果。

易错点:不要把执行器函数当成异步回调。它是同步执行的;真正异步的是后面 then 里的回调。转写稿里也特别区分过:then/catch/finally 的回调才是异步任务,而 new Promise 的执行器不是。

② 修改 promise 对象的状态 ​

为什么需要:Promise 必须有明确结果,后续 then 才知道该执行成功回调还是失败回调。状态就是“成功/失败”的信号。

怎么想:调用执行器的第一个参数 resolve,状态变为 resolved(也叫 fulfilled);调用第二个参数 reject,状态变为 rejected。传给它们的参数会成为 PromiseResult,后面回调可以拿到。

怎么做:

js
new Promise((resolve, reject) => {
    // 调用第一个参数 该promise对象的状态改为 resolved(fulfilled)
    // 可以传个参数作为 PromiseResult
    // resolve('hello');
    // resolve({status:'OK', msg: 'success'});

    // 调用第二个参数, 该promise对象的状态改为 rejected
    // 可以传个参数作为 PromiseResult
    // reject();
    reject([10,20,30,40]);
});

易错点:状态一旦改变,就无法再修改。比如先 resolve 再 reject,后面这次不会把状态改回去。另一个易错点是:resolve/reject 的参数不是随便丢的,它会成为 PromiseResult,后面 then 的形参拿到的就是它。

③ 为 promise 对象设置回调函数 ​

为什么需要:只创建和改状态还不够,我们最终要拿到异步结果。then 就是给 Promise 设置成功/失败回调的方法。

怎么想:then 的第一个参数是成功回调,状态变为成功时执行;第二个参数是失败回调,状态变为失败时执行。两个回调都能通过形参得到 PromiseResult。并且 then 的两个回调都是异步执行,不会立刻同步执行。

怎么做:

js
promise对象.then(res => {
    // 如果promise对象是成功状态,执行该回调函数
    // res 可以获取 PromiseResult
}, err => {
    // 如果promise对象是失败状态,执行该回调函数
    // err 可以获取 PromiseResult
});

笔记原注释第二个回调里写成了 res,这里应理解为 err:失败回调的形参接收失败原因或失败值。

易错点:第一,then 的回调是异步的,不要以为写了 then 就马上执行;第二,失败回调是 then 的第二个参数,不是第一个;第三,后面学 async/await 时,await 右侧如果是成功状态的 Promise,会等状态改变后取 PromiseResult,如果是失败状态会抛出异常。这正是转写稿里说的“async/await 是 Promise 的语法糖”。所以本节三件事不牢,后面 await 的等待、取值、异常处理都会混乱。

小结 ​

本节就三句话:new Promise 的执行器同步执行;resolve/reject 改状态并携带 PromiseResult;then 注册成功/失败回调,且回调异步执行。先把这三步跑通,再去学 async/await 代替 then 链式调用,才不会把“等待”误认为“阻塞主线程”。

本节定位:从 Promise 实例方法到 async/await ​

本节从 Promise 实例方法的使用继续往前推进:then 链式调用虽然能处理异步结果,但仍要写回调函数;ECMAScript 设计者于是提供了 async 和 await 这种语法糖。为什么需要:减少 then 回调,让异步流程更像同步代码。怎么想:async 函数一定返回 Promise,await 用来等待 Promise 状态改变并取出结果,底层仍是 Promise 的状态管理。怎么做:先掌握 async 函数返回值规则,再掌握 await 取值规则,最后用它替代 then 链式调用和部分 Promise.all 场景。易错点:async/await 不是不要 Promise,而是换一种写法使用 Promise;await 不能随便写在普通函数里。

async 函数:任何函数形式都能加 async ​

为什么需要:统一异步函数的声明方式,让函数内部可以使用 await。怎么想:async 是关键字,加在函数声明最前面即可;函数声明、函数表达式、箭头函数、立即执行函数、对象方法都可以变成 async 函数。怎么做:

js
async function fn1() {}
const fn2 = async function () {};
const fn3 = async () => {};
(async () => {})();
const obj = {
  async say() {}
};

易错点:关键字位置要找对——普通函数写在 function 前,箭头函数写在小括号前,对象方法简写写在方法名前。另一个常见错误是“函数写了却没调用”,转写稿演示中就因为没有调用 async 函数而看不到输出。async 函数只有被调用才执行。

async 函数的返回值:四条规则 ​

为什么需要:async 函数返回的不是普通值,而是 Promise,必须知道这个 Promise 的状态和结果由谁决定。怎么想:async 函数内部 return 的值会影响返回 Promise 的状态;抛异常则变成失败状态。怎么做:规则可归纳为四条:

js
const f1 = async () => {};
f1().then(v => console.log(v)); // 成功,undefined

const f2 = async () => 100;
f2().then(v => console.log(v)); // 成功,100

const f3 = async () => Promise.resolve(400);
f3().then(v => console.log(v)); // 跟随返回的 Promise,成功,400

const f4 = async () => {
  throw new Error('出错了');
};
f4().catch(e => console.log(e.message)); // 失败,Error 对象

易错点:不是“return 什么就原样返回什么”,而是 return 影响返回 Promise 的状态;返回 Promise 时,外层 Promise 会跟随内层 Promise;async 函数内抛异常会导致返回 Promise 变为失败状态,可用 catch 捕获。

await 表达式:取值与等待 ​

为什么需要:不想再在 then 回调里拿结果,希望用变量直接接住 Promise 的结果。怎么想:await 与右侧表达式共同组成 await 表达式;它必须写在 async 函数中,一个 async 函数中可以有零个或多个 await。怎么做:

js
async function main() {
  const r1 = await 100; // 非 Promise,值就是 100
  const r2 = await Promise.resolve(400); // 成功 Promise,取 result:400
  console.log(r1, r2);
}
main();

右侧是成功状态 Promise 时,要等状态改变后才能取到值,值就是 Promise 的 result;右侧是失败状态 Promise 时,await 表达式取不到值,并抛出异常,后续代码中断。处理失败可写 try/catch:

js
async function main() {
  try {
    const r = await Promise.reject('失败');
  } catch (e) {
    console.log(e);
  }
}

易错点:await 不阻塞主线程,但会阻塞当前 async 函数内后续代码;多个 await 是串行等待,上一个不结束,下一个不开始;失败 Promise 必须捕获,否则会中断并表现为异常。

用 async/await 代替 then 链式调用 ​

为什么需要:then 链式调用仍要写回调函数,async/await 可以让步骤更直观。怎么想:把每一步的 then 改成 await,用变量接收上一步结果,下一步直接使用变量。怎么做:

js
(async () => {
  const res1 = await ajax('/list');
  const res2 = await ajax(`/songs?listId=${res1[0].id}`);
  const res3 = await ajax(`/detail?songId=${res2.id}`);
  console.log(res3);
})();

易错点:第二步必须等第一步结束,第三步必须等第二步结束;中间某一步失败会中断后续;变量作用域要写清楚,避免拿错上一步结果。

用 async/await 代替 Promise.all:注意 forEach 的坑 ​

为什么需要:批量获取详情时,既可以用 Promise.all,也可以用 await 逐个请求。怎么想:await 写在循环里会按顺序等待;但不要把 await 写进 forEach 回调里,因为 forEach 不会等待异步回调。怎么做:用 for 循环:

js
const data = [];
for (let i = 0; i < 10; i++) {
  const res = await ajax(`/detail?id=${ids[i]}`);
  data.push(res);
}

易错点:转写稿专门演示了 forEach 中写 await 的问题——外层代码不会等它,打印数组时还是空的;调试工具点开后又看到有值,是因为展开时显示的是当前状态,不代表输出那一刻已有值。用 for 循环才能保证按顺序等待。

微队列与宏队列:async/await 背后的调度 ​

为什么需要:理解异步回调的执行顺序,才能解释 await、then、定时器、DOM 事件的先后。怎么想:异步任务回调分微任务和宏任务;按较新的 W3C 标准,宏队列又被拆成交互队列和延迟队列,但“宏队列”仍是常见民间叫法。怎么做:微队列包括 Promise 的 then/catch/finally 回调、MutationObserver 等;交互队列包括 DOM 事件回调;延迟队列包括定时器回调、Ajax 回调。执行优先级为:同步代码之后,先微队列,再交互队列,最后延迟队列。易错点:Promise 构造器里的执行器函数不是异步;then/catch/finally 的回调才进入微队列。主线程空后才按优先级取队列任务,不要把所有异步任务混成一类。

async 函数的声明形式 ​

为什么需要:Promise 的 then 链式调用能解决回调地狱,但仍要手动指定回调函数。ECMA 设计 async/await 这层“语法糖”,让异步代码写起来更像同步代码。
怎么想:先明确 async 是加在函数声明前的关键字,任何形式的函数都可以变成 async 函数。
怎么做:

js
async function foo() {}

const bar = async function () {};

const baz = async () => {};

(async () => {
  console.log('立即执行的 async 函数');
})();

const obj = {
  async say() {}
};

易错点:关键字位置要找准——普通函数写在 function 前,函数表达式写在 function 前,箭头函数写在小括号前,对象方法写在方法名前。调试时如果函数没被调用,里面的代码当然不会执行。

async 函数的返回值规则 ​

为什么需要:async 函数不是普通函数,它固定返回 Promise,必须弄清返回的 Promise 状态由谁决定。
怎么想:async 函数内部 return 的值不能直接当普通返回值看,它会影响返回的 Promise 的状态和结果。规则有四条:没有 return,返回成功状态、结果为 undefined;return 非 Promise 值,返回成功状态、结果为该值;return 一个 Promise,则跟随这个 Promise 的状态;函数内抛出异常,返回失败状态、结果为异常对象。
怎么做:

js
async function fn() {}
fn().then(v => console.log('成功', v), e => console.log('失败', e));

async function f1() { return 100; }
async function f2() { return Promise.resolve(400); }
async function f3() { throw new Error('出错了'); }

f1().then(v => console.log(v)); // 100
f2().then(v => console.log(v)); // 400
f3().catch(e => console.log(e.message)); // 出错了

易错点:不要以为 async 函数里的 return 还能像普通函数那样直接拿到值;它返回的始终是 Promise。调试时先看状态,再看 PromiseResult。

await 表达式与取值 ​

为什么需要:有了 async 函数返回 Promise,还需要一种方式在函数内部“等”Promise 的结果,await 就是干这个的。
怎么想:await 不是函数,是表达式,必须写在 async 函数中;右侧可以跟一个表达式,整个 await 表达式有值。右侧是非 Promise,值就是右侧表达式的值;右侧是成功状态的 Promise,会等状态改变后取到 PromiseResult;右侧是失败状态的 Promise,会抛出异常,取不到值,后续代码中断。
怎么做:

js
async function main() {
  const r1 = await 100; // 100
  const r2 = await Promise.resolve(400); // 400
  console.log(r1, r2);
}
main();

易错点:await 只能写在 async 函数里,不能随便写在顶层;async 函数里可以有零个或多个 await,但反过来不行。调试时如果没输出,先检查函数是否被调用。

await 处理失败 Promise ​

为什么需要:实际请求可能成功也可能失败,失败 Promise 在 await 时会抛异常,必须处理。
怎么想:两种方式——用 try/catch 包住 await;或利用 async 函数返回失败 Promise,再在外部用 catch 捕获。
怎么做:

js
async function main() {
  try {
    const r = await Promise.reject('失败');
    console.log(r); // 取不到值
  } catch (e) {
    console.log('try/catch 捕获', e);
  }
}
main();

async function main2() {
  const r = await Promise.reject('失败');
}
main2().catch(e => console.log('catch 捕获', e));

易错点:失败时 await 表达式取不到值,调用会中断;不要以为还能继续执行下一行。

用 async/await 替代 then 链式调用 ​

为什么需要:多个异步步骤有先后依赖时,then 链式调用仍然啰嗦。
怎么想:把每一步请求前加 await,用变量接住结果,下一步直接用上一步结果。
怎么做:

js
(async () => {
  const res1 = await 获取榜单();
  const res2 = await 获取歌曲列表(res1[0].id);
  const res3 = await 获取歌曲详情(res2.songId);
  console.log(res3);
})();

易错点:第二步必须等第一步结束,第三步必须等第二步结束;await 让它们在 async 函数内部按顺序排队,但不阻塞主线程。

用 async/await 替代 Promise.all ​

为什么需要:批量请求时常用 Promise.all,也可以用 async/await 改写。
怎么想:需要保证顺序时,用 for 循环逐个 await,把结果 push 到数组。
怎么做:

js
(async () => {
  const list = await 获取歌曲列表();
  const data = [];
  for (let i = 0; i < 10; i++) {
    const item = await 获取歌曲详情(list[i].id);
    data.push(item);
  }
  console.log(data);
})();

易错点:不要用 forEach 包 await,它的回调不是 async 上下文,也不会等待;调试时可能看到数组先为空,点开后又有值,这是异步追加和调试工具展示造成的。

微队列与宏队列 ​

为什么需要:理解异步回调的执行顺序,才能解释 Promise、定时器、DOM 事件的输出顺序。
怎么想:异步任务分微任务和宏任务。微队列放 Promise 的 then/catch/finally、MutationObserver;宏队列按新标准可拆成交互队列(DOM 事件)和延迟队列(定时器、Ajax)。优先级是:同步代码 → 微队列 → 交互队列 → 延迟队列。
怎么做:先执行完同步代码,主线程空了再依次清空微队列,再处理交互队列,最后处理延迟队列。
易错点:W3C 新标准不再强调“宏队列”这个词,但民间仍沿用;不要把它理解成阻塞主线程,异步回调仍是在主线程空出来后排队执行。

先抓住一条主线 ​

本节把 Promise 使用中最容易混淆的 7 个问题串起来。转写稿里讲师反复提到“链式调用像排队,上一个不完成,下一个不开始”,也反复纠正 permiss/permise 应为 Promise、Z 应为 then。理解这 7 点,后面看 async/await 才不会觉得是另一套东西。

1. 如何改变 Promise 的状态 ​

为什么需要:Promise 的核心就是状态管理,状态不变,回调就不知道何时执行。怎么想:一个 Promise 初始是 pending,只有 pending 才能被改变,而且改完就定型。怎么做:在执行器中调用 resolve(value),状态从 pending 变为 resolved;调用 reject(reason),变为 rejected;执行器里抛出异常,也等价于进入 rejected。易错点:不要以为 resolve 是“返回结果”,它改变的是状态;状态已经改变后再调用 resolve/reject 不会反复切换。

2. 一个 Promise 实例指定多个成功/失败回调,都会调用吗 ​

为什么需要:同一个异步结果可能被多个地方订阅。怎么想:Promise 不是“只能被 then 一次”,而是状态落定后,对应队列里的回调都会被调用。怎么做:对同一个实例多次写 then,成功状态会调用各成功回调,失败状态会调用各失败回调。易错点:不是“只执行第一个”;但成功回调只接成功,失败回调只接失败或异常,不能混着接。

3. 改变状态和指定回调谁先谁后 ​

为什么需要:很多时序问题都出在这里。怎么想:两种顺序都可能。正常写法是先指定回调,再异步改变状态;也可以先改状态,再指定回调。怎么做:如果执行器里直接 resolve(),就是先改状态;如果延迟更久才 then(),就是后指定回调。易错点:先改状态再 then,回调不会“错过”,仍会在之后执行并拿到数据;数据不是“状态一变就立刻在同步代码里出现”,而是在回调执行时拿到。

4. then 返回的新 Promise 结果状态由什么决定 ​

为什么需要:链式调用能不能继续,全靠 then 返回的新 Promise。怎么想:看 then 里回调函数的执行结果,而不是看原 Promise。怎么做:没写返回值,新 Promise 为 resolved,值为 undefined;返回非 Promise 值,新 Promise 为 resolved,值为该返回值;返回另一个 Promise,新 Promise 跟随它的结果;抛出异常,新 Promise 为 rejected,原因为异常。易错点:then 返回的不是原实例;回调里不 return,下游只能拿到 undefined。

5. Promise 如何串连多个操作任务 ​

为什么需要:避免回调地狱,让多个异步任务按顺序接力。怎么想:then() 返回新 Promise,所以可以继续 .then()。怎么做:把任务写成 then 的链式调用,每一步把下一步需要的数据 return 出去。转写稿里说“一个等一个”,正是这个意思:上一步不结束,下一步不开始。易错点:每一步该返回的值或 Promise 必须返回;否则下游拿不到依赖数据。

6. Promise 异常传透 ​

为什么需要:链上任何一步出错,都要有统一出口。怎么想:前面操作抛异常或返回失败 Promise,会沿着链往后传,跳过普通成功回调。怎么做:在链的最后指定失败回调,集中处理前面任何环节的异常。易错点:如果中间某个 then 自己写了失败回调并处理掉,异常就不会继续往后传;所以“传到最后”的前提是中间没有截住。

7. 中断 Promise 链 ​

为什么需要:某些条件下不想继续执行后面的任务。怎么想:让链停在一个永远 pending 的 Promise 上,后面的 then 就不会被调用。怎么做:在回调中返回一个 pending 状态的 Promise 实例。易错点:返回 undefined 会继续走且变成成功;抛出异常会走失败回调,不是“中断”;返回 pending 后要有意识:它会一直等,转写稿里说的“一直等着,后边不执行”就是这种效果。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

1. async 函数的各种声明形式与返回值规则 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>async 函数与返回值</title>
</head>
<body>
<script>
  /* ===== 一、任何形式的函数加上 async 关键字,都是 async 函数 ===== */
  async function fn1() {}                    // 声明式:写在 function 前
  const fn2 = async function () {};          // 函数表达式:写在 function 前
  const fn3 = async () => {};                // 箭头函数:写在小括号前
  (async () => {                             // 立即执行的 async 函数
    console.log('立即执行的 async 函数');
  })();
  const user = { async say() {} };           // 对象中的方法:写在方法名前

  /* ===== 二、async 函数的返回值一定是 Promise 对象 ===== */
  // 情况一:没有 return
  const fnA = async () => {};

  // 情况二:return 非 Promise 类型的数据
  const fnB = async () => { return 100; };

  // 情况三:return 一个 Promise 对象(这里用 reject 快速创建一个失败的 Promise)
  const fnC = async () => { return Promise.reject(250); };

  // 情况四:函数体内部抛出异常(xxx 是未定义的函数)
  const fnD = async () => { xxx(); };

  fnA().then(res => console.log('A 成功:', res), err => console.log('A 失败:', err));
  fnB().then(res => console.log('B 成功:', res), err => console.log('B 失败:', err));
  fnC().then(res => console.log('C 成功:', res), err => console.log('C 失败:', err));
  fnD().then(res => console.log('D 成功:', res), err => console.log('D 失败:', err));
</script>
</body>
</html>

关键行与易错点

  • async 关键字的位置要找准:声明式/表达式写在 function 前,箭头函数写在小括号前,对象方法写在方法名前,总之写在最前面。
  • fnA 的结果:状态 resolved,PromiseResult 为 undefined。
  • fnB 的结果:状态 resolved,PromiseResult 为 100(返回值直接作为结果)。
  • fnC 的结果:状态 rejected,PromiseResult 为 250 —— 当 return 的是 Promise 对象时,它的状态和结果就是 async 函数返回的 Promise 的状态和结果。
  • fnD 的结果:状态 rejected,PromiseResult 为抛出的异常对象 —— 函数体内抛异常不会让整个脚本崩掉,而是变成返回的 Promise 的失败原因。
  • 注意:async 函数体内部代码是同步执行的,只有 then 的回调才是异步(微任务)执行的。

2. await 表达式的三种取值情况 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>await 表达式取值</title>
</head>
<body>
<script>
  // 一个 3 秒后才改变状态的 Promise
  const p = new Promise((resolve) => {
    setTimeout(() => { resolve(600); }, 3000);
  });

  async function main() {
    // 情况一:await 右侧是非 Promise → 表达式值就是右侧表达式的值
    const res1 = await 100;
    console.log('res1:', res1);                 // 100

    // 情况二:await 右侧是成功状态的 Promise → 等状态改变后,值就是 PromiseResult
    const res2 = await Promise.resolve(400);
    console.log('res2:', res2);                 // 400

    const res3 = await p;                        // 3 秒后才拿到值
    console.log('res3:', res3);                 // 600

    // 情况三:await 右侧是失败状态的 Promise → 抛出异常,取不到值
    try {
      const res4 = await Promise.reject('请求失败了');
      console.log('res4:', res4);               // 不会执行
    } catch (e) {
      console.log('try/catch 捕获:', e);
    }
  }

  main();

  /* 处理失败的第二种方式:async 函数内部抛异常 → 返回的 Promise 变 rejected → 用 catch 接 */
  const main2 = async () => {
    await Promise.reject('又失败了');
  };
  main2().catch(reason => console.log('.catch 捕获:', reason));
</script>
</body>
</html>

关键行与易错点

  • await 关键字和它右边的表达式共同组成一个 await 表达式,它本身是有值的。
  • await 只能写在 async 函数内部,直接写在顶层会报错:await is only valid in async functions and the top level bodies of modules。
  • 右侧是 pending 的 Promise 时,await 会一直等,状态不改变就一直拿不到值,后面的代码也不执行。
  • 右侧是 rejected 的 Promise 时,会抛出异常并中断当前 async 函数,取不到值,所以必须用 try...catch 包住,或者对 async 函数返回的 Promise 使用 .catch()。两种方式都可以。
  • async 函数里可以没有 await(可以有零个或多个),但反过来不行。
  • await 只是让当前 async 函数内部排队等待,并不会阻塞主线程。

3. async/await 代替 then 链式调用,以及代替 Promise.all ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>async/await 代替链式调用</title>
</head>
<body>
<script>
  // 用定时器模拟 ajax 请求,返回 Promise 对象
  function request(url, time = 1000) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(url + ' 的数据');
      }, time);
    });
  }

  (async () => {
    /* ===== 一、代替 then 的链式调用 ===== */
    // 第一步:获取榜单列表
    const res1 = await request('/api/toplist');
    console.log('榜单:', res1);

    // 第二步:根据第一步的结果获取歌曲列表
    const res2 = await request('/api/songlist?id=' + res1);
    console.log('歌曲列表:', res2);

    // 第三步:根据第二步的结果获取歌曲详情
    const res3 = await request('/api/song/detail?id=' + res2);
    console.log('歌曲详情:', res3);

    /* ===== 二、代替 Promise.all ===== */
    const ids = [1, 2, 3, 4, 5];
    const data = [];
    for (let i = 0; i < ids.length; i++) {
      // for 循环体在 async 函数作用域里,可以直接 await,顺序有保证
      data.push(await request('/api/song/' + ids[i], 500));
    }
    console.log('所有歌曲详情:', data);
  })();
</script>
</body>
</html>

关键行与易错点

  • 用立即执行的 async 箭头函数 (async () => { ... })() 包裹,因为 await 必须写在 async 函数里。
  • 链式调用里 then 回调函数的 res 参数,现在直接变成 await 表达式的值,不用再写回调函数,这就是 async/await 作为 Promise 语法糖的甜处。
  • 三步之间是串行的:第一步不结束,第二步不开始,顺序由代码结构直接保证。
  • 易错点:forEach 里不能直接 await,会报语法错误;给 forEach 的回调加上 async 也不行,因为 forEach 不会等待回调完成,外层会同步往下执行,数组还是空的。
  • 易错点: 如果控制台打印 data 时是空数组、点开却又有值,那是引用类型 + 异步追加导致的调试显示问题,输出那一刻它确实是空的。
  • 想替代 Promise.all 时,for 循环才是可靠的:循环体没有额外包一层函数,await 就在外层 async 函数作用域里,一个一个执行完循环才结束。

常见错误 ​

1. await 写在普通函数里

  • 现象:Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules
  • 原因:await 只能出现在 async 函数体内(或模块顶层)
  • 修法:给外层函数加 async;没有外层函数时写成 (async () => { ... })()

2. 在 forEach 里 await,结果收集不全

  • 现象:await 之后的代码先执行,打印出的数组是空的;点开控制台再看又有值;多条请求顺序混乱
  • 原因:forEach 的回调被单独包成了一个函数,forEach 本身不等它,外层 async 函数管不到里层
  • 修法:换成 for / for...of 循环(在同一个 async 函数里逐个 await),或改成 Promise.all(arr.map(...)) 并发

3. await 一个失败状态的 Promise 却没捕获

  • 现象:控制台 Uncaught (in promise),后续代码全部中断
  • 原因:await 右侧是 rejected 时不会给你值,而是直接抛异常;async 函数返回的 Promise 也随之变成失败状态
  • 修法:把 await 表达式放进 try / catch;或在调用 async 函数的地方给返回的 Promise 挂 .catch()

4. 以为 const res = fn() 能直接拿到数据

  • 现象:res 打印出来是 Promise {<fulfilled>: ...},res.xxx 是 undefined
  • 原因:async 函数永远返回 Promise,return 的值只是这个 Promise 的 PromiseResult
  • 修法:const res = await fn(),或 fn().then(res => ...)

5. 写了 async 函数却没调用 / IIFE 忘了加 ()

  • 现象:一行输出都没有,以为是语法或 await 的问题
  • 原因:函数只是被声明,函数体根本没执行;async () => {...} 只写了函数本身
  • 修法:末尾补 ();需要时写成 (async () => {...})()

6. 把 await 当成阻塞主线程

  • 现象:推断输出顺序时出错,以为 await 期间定时器、DOM 事件回调都不跑
  • 原因:await 只暂停当前这个 async 函数,主线程照样往下跑,恢复执行时走的是微队列
  • 修法:按事件循环推演——同步代码 → 微队列(then/catch/await 恢复)→ 交互队列(DOM 事件)→ 延迟队列(定时器、Ajax)

练习 ​

1. 改写题 把下面这段改写成 async/await 形式,要求不出现 then:

js
getBanner().then(res => getSongs(res[0].id))
           .then(res => getDetail(res.sid))
           .then(res => console.log(res));

2. 找错题 指出问题并改正,要求:按顺序拿到全部结果。

js
async function getData(ids) {
  const list = [];
  ids.forEach(async id => {
    const res = await fetchItem(id);
    list.push(res);
  });
  console.log(list);
}

3. 异常处理题 写一个立即执行的 async 函数:依次请求「榜单 → 榜单歌曲 → 歌曲详情」三步,任意一步失败都要打印失败原因,且不产生未捕获的 Promise 错误。给出 try/catch 和 .catch() 两种写法。

4. 并发改造题 把第 3 题的第 2 步(拿到 10 个歌曲 id 后取详情)改成并发请求,用 Promise.all 与 for 循环 + await 各写一版,并说明两者在耗时和顺序上的差别。

5. 输出顺序题 写出输出顺序并说明理由:

js
console.log(1);
setTimeout(() => console.log(2), 0);
(async () => {
  console.log(3);
  await null;
  console.log(4);
})();
Promise.resolve().then(() => console.log(5));
console.log(6);

原视频对照 ​

#视频时长
1声明async函数06:51
2async函数的返回值06:35
3await表达式27:12
4async和await代替then的链式调用05:54
5async和await代替Promise.all方法14:35
6微队列和宏队列14:43

小结 ​

  1. async 是加在函数声明最前面的修饰符,任何形式的函数(声明式、表达式、箭头、IIFE、对象方法)都能变成 async 函数。
  2. async 函数永远返回 Promise,return 的内容只决定这个 Promise 的状态和结果,不影响"它是不是 Promise"。
  3. await 是表达式而不是函数,必须写在 async 函数内部;一个 async 函数里可以有 0 个或多个 await。
  4. await 右侧是非 Promise 值时,表达式的值就是右侧的值;是成功状态的 Promise 时,值是该 Promise 的 PromiseResult,且要等状态改变才拿得到。
  5. await 右侧是失败状态的 Promise 时,取不到值、直接抛异常并中断当前 async 函数——所以必须配 try/catch 或对返回值 .catch()。
  6. await 的价值在于把"层层回调"压平成看起来同步的代码,但它本质仍是异步:不阻塞主线程,只让当前函数内部按顺序排队。
  7. 想并发就用 Promise.all / map;想串行就用 for 循环逐个 await——forEach 里 await 两者都做不到。
  8. await 之后的代码在微队列里恢复执行,优先级高于 DOM 事件(交互队列)和定时器 / Ajax(延迟队列),推导输出顺序时按这个层级来。