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