第一周04天Promise 深入与 async / await

两小时学习讲义:Promise 深入与 async / await

配套《学习计划.md》使用,本讲义把每个知识点打碎到颗粒级 ,并逐点给出类比 + 例子 + 真实开发场景 。 学习方法建议:先猜后跑。看到异步代码先写下输出/结果,再运行验证。 全程约 120 分钟,与学习计划的时间轴一一对应。


⏱️ 时间轴总览

时间段 环节 本讲义位置
20:00--20:10 🔁 回顾与预习 第 1 段
20:10--20:25 📚 知识块①:Promise 状态机与链式调用 第 2 段
20:25--20:40 📚 知识块②:all / allSettled / race / any 第 3 段
20:40--20:50 📚 知识块③:async/await 本质与错误处理 第 4 段
20:50--21:00 📝 费曼复述 第 5 段
21:00--21:20 🛠️ 练习①:手写 myPromiseAll 第 6 段
21:20--21:40 🛠️ 练习②③:sleep 串并行 + withTimeout 第 7 段
21:40--21:50 💭 思考提高题 第 8 段
21:50--22:00 ✅ 测验 第 9 段

第 1 段(10min)回顾与预习:接住昨天的线

1.1 先接昨日(事件循环)的线

昨天你学了 Promise.then微任务await 之后的代码会进微任务队列。今天把这些"知道怎么用"的东西推向"懂原理":

javascript 复制代码
周一 闭包        变量看定义处
周二 this        调用看调用处
周三 原型链      属性看原型链
周四 事件循环    代码看事件循环(谁排队、谁先走)
周五 Promise    异步的控制流怎么组织(状态、组合、语法糖)

周四你记的"铁律"今天会反复用到:微任务先于下一个宏任务。Promise 的所有时序行为都建立在这条之上。

1.2 先凭直觉回答 4 个小问题(不用对,先写下答案)

js 复制代码
// 问题 1:Promise 状态能改第二次吗?
const p = new Promise((resolve, reject) => {
  resolve('成功');
  reject('失败');   // 这行有效吗?
});
p.then(v => console.log(v), r => console.log('rejected', r));
// 输出?

// 问题 2:new Promise 的回调什么时候执行?
console.log('1');
new Promise(() => console.log('2'));
console.log('3');
// 顺序?

// 问题 3:下面会输出什么?
Promise.resolve()
  .then(() => console.log('A'))
  .then(() => console.log('B'));
console.log('C');
// 顺序?

// 问题 4:await 用在普通函数里?
function f() { await Promise.resolve(); }   // 会怎样?

学完第 2、4 段再回来对照。第 1 段只做一件事:记住 Promise 是"承诺"------先发凭证,落定后不可更改。


第 2 段(15min)知识块①:Promise 状态机与链式调用

2.1 三种状态与"不可逆"

知识点(细化)

  • Promise 有三个状态:pending(进行中)→ fulfilled(已成功,持有 value)或 rejected(已失败,持有 reason)。
  • 状态只能落定一次,不可逆pending 一旦变成 fulfilled 或 rejected 就永久锁定,再调用 resolve/reject 都无效。
  • 这保证了 .then 的回调最多执行一次------不会出现"又成功又失败"的乱象。
  • new Promise(executor) 的 executor(执行器)是同步执行的------new 的那一瞬间就跑。

例子:先猜后跑

js 复制代码
const p = new Promise((resolve, reject) => {
  resolve('成功');   // 状态 → fulfilled
  reject('失败');    // 无效:已落定,不可再改
});
p.then(v => console.log(v), r => console.log('rejected', r));
// 只输出:成功

// executor 同步执行
console.log('1');
new Promise(() => console.log('2'));
console.log('3');
// 1 2 3(executor 是同步的)

类比(泼出去的水 / 发出去的合同) :resolve/reject 像签合同------签完就生效,不能反悔。就算你后面再撕掉重签,第一次签字的内容也作废不了。

真实场景:为什么 fetch 的结果只能用一次?因为响应 Promise 一旦落定就锁定。这也是为什么"缓存一个 fetch 的结果"常常要存数据而不是存 Promise。

2.2 链式调用:.then 永远返回新 Promise

知识点(细化)

  • .then返回一个新的 Promise ,所以可以无限 .then 串联。
  • 链上的每个回调的返回值会传给下一个 .then
  • 拍平(assimilation) :如果回调返回的是一个 Promise ,外层不会直接把它当结果,而是等它落定后再继续------这正是"把异步流程串起来"的根基。
  • 失败不会自动跳过:rejected 会跳过后续 .then 的成功回调,直接落到最近的失败处理(第二个参数 / .catch)。

例子:先猜后跑

js 复制代码
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }

delay(1000)
  .then(() => { console.log('1s'); return delay(1000); })  // 返回 Promise → 被拍平
  .then(() => console.log('2s'));                          // 等它完成才执行
// 1s(等1秒)→ 2s(再等1秒)

类比(接力棒).then 像接力跑------上一棒(回调)把"下一段任务"(返回的 Promise)交给你,你拿着它跑完(等它落定),再递给下一棒。每一棒都是一个新的 Promise,所以想接多少棒接多少棒。

真实场景:登录 → 拉取用户信息 → 拉取权限 → 渲染页面,四步异步流程用链式串联:

js 复制代码
login()
  .then(token => fetchUser(token))
  .then(user => fetchPerms(user.id))
  .then(perms => render(perms));

如果没有"拍平",你写不了这种链------每步都要手动嵌回调,就是"回调地狱"。

2.3 值穿透与 finally

知识点(细化)

  • 不带回调的 .then() 会把 value 原样透传下去。
  • finally 无论成功失败都执行(适合收尾),但透传结果(不改 value/reason)。

例子

js 复制代码
Promise.resolve('A')
  .finally(() => console.log('finally 一定会执行'))
  .then(v => console.log('值仍透传:', v));
// finally 一定会执行
// 值仍透传:A

真实场景 :loading 开关、清理定时器/事件监听这种"无论成败都要做"的收尾,放进 finally

js 复制代码
showLoading();
fetchData()
  .then(render)
  .catch(showError)
  .finally(() => hideLoading());   // 成也执行、败也执行

第 3 段(15min)知识块②:all / allSettled / race / any

3.1 四兄弟总表(背下来)

知识点(细化)

方法 等谁 失败行为 返回值 适用
Promise.all 全部成功 任一失败立即 reject(快速失败) 成功时的结果数组(按传入顺序) 全部成功才继续(并行初始化)
Promise.allSettled 全部落定(不论成败) 永不 reject 每项 {status, value/reason} 想要每个任务的最终结果
Promise.race 最先落定的那个(成功失败都算) 先落定者决定结果 先落定者的 value/reason 超时控制、竞速
Promise.any 第一个成功 全部失败才 reject(抛 AggregateError 第一个成功的 value 只要有一个成功即可

共同点 :传入的数组里可以混普通值(会自动 Promise.resolve 包装);传非迭代对象会报错。

类比(四种开会方式)

  • all = 团队全体到齐才开工;有一个人请假(失败)就整组解散返工。
  • allSettled = 每位成员的最终报告都收齐------有人搞砸了也照样收,标注"失败"。
  • race = 赛跑,谁先到终点算谁(到就行,不管它是不是摔了)。
  • any = 海选,只要有一个晋级就收工;全部淘汰才重来。

3.2 逐个验证(先猜后跑)

Promise.all:快速失败 + 顺序一致

js 复制代码
// 顺序一致:结果数组按传入顺序,不按完成先后
const slow = new Promise(r => setTimeout(() => r('慢'), 1000));
const fast = new Promise(r => setTimeout(() => r('快'), 100));
Promise.all([slow, fast]).then(([a, b]) => console.log(a, b));  // '慢 快'
// 慢先完成?不------结果是按传入顺序 [slow, fast],所以 a='慢' b='快'

// 快速失败:一个 reject,整组 reject
Promise.all([Promise.resolve(1), Promise.reject('boom')])
  .then(v => console.log('不会到这', v))
  .catch(e => console.log('快速失败:', e));   // 快速失败: boom

Promise.allSettled:永不 reject

js 复制代码
Promise.allSettled([Promise.resolve(1), Promise.reject('err')]).then(console.log);
// [{status:'fulfilled',value:1}, {status:'rejected',reason:'err'}]

Promise.race:先落定者胜

js 复制代码
Promise.race([new Promise(r => setTimeout(() => r('慢'), 1000)), Promise.resolve('快')])
  .then(console.log);   // 快

Promise.any:第一个成功者胜;全败才 AggregateError

js 复制代码
Promise.any([Promise.reject('a'), Promise.resolve('b')]).then(console.log);   // b
Promise.any([Promise.reject('a'), Promise.reject('b')])
  .catch(e => console.log(e instanceof AggregateError));   // true

3.3 关键坑:all 的"快速失败"不会取消其他请求

知识点(细化)Promise.all 遇到一个失败立即 reject,但其余 Promise 仍在后台执行 ------它们的结果只是不再被采纳。要"真正取消",all 做不到,需要 AbortController(第 8 段思考题①展开)。

真实场景

  • all:页面初始化需要"配置 + 用户信息"都成功才渲染。
  • allSettled:批量上报埋点,个别失败不影响整体(仍要拿到每条的成败)。
  • race:请求超时控制、图片加载超时。
  • any:CDN 多源取资源、头像多源获取------有一个成功就用它。

第 4 段(10min)知识块③:async/await 本质与错误处理

4.1 async 函数:总是返回 Promise

知识点(细化)

  • async function 声明的函数总是返回一个 Promise(return 普通值会被自动包装)。
  • 内部 throw → 返回的 Promise 变成 rejected。

例子

js 复制代码
async function f() { return 42; }
f() instanceof Promise;           // true ------ 普通值被包装

async function g() { throw new Error('boom'); }
g().catch(e => console.log(e.message));  // boom ------ 抛错变 rejected

4.2 await:.then 的语法糖,后续代码进微任务

知识点(细化)

  • const r = await pp.then(r => ...):等右侧 Promise 落定,把结果取出来。
  • await 之后的代码会被包装成微任务(这就是昨天学的"await 后续是微任务")。
  • await暂停当前 async 函数不阻塞主线程------等待期间其他代码照常执行。
  • 语法限制:await 只能用在 async 函数内(或 ES 模块顶层),普通函数里用直接 SyntaxError

例子:await 不阻塞主线程(先猜后跑)

js 复制代码
async function test() {
  console.log('A');
  await Promise.resolve();
  console.log('B');     // 微任务
}
console.log('C');
test();
console.log('D');
// C A D B

类比(排队的你把流程暂停,但队伍照走) :你在餐厅点了菜(发起 Promise),await = 你坐下等菜------只是你自己暂停点菜流程,别人(主线程的其他代码、渲染、事件)照常进行。等菜上了(Promise 落定),你接着做(微任务)。

真实场景await 不会卡 UI------发起一个 3 秒请求时,页面还能滚动、按钮还能响应,正是因为 await 让出了主线程。

4.3 错误处理:try/catch 是首选

知识点(细化)

  • await 一个 rejected Promise → 当场抛异常 ,可被 try/catch 捕获。
  • async 函数内的 throw → 变成 rejected Promise(可在调用处 .catch)。
  • 两种兜底:函数内部 try/catch(推荐,就近处理)+ 调用处 .catch(兜住漏网的)。

例子

js 复制代码
async function load() {
  try {
    const res = await fetch('/api');
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();
    return data;
  } catch (err) {
    console.error('加载失败', err);
    return null;              // 兜底返回,不让错误上抛
  }
}

类比(安全气囊)try/catch 是安全气囊------撞了(异步失败)自动弹开,驾驶员(程序)不至于车毁人亡(未捕获的 rejection)。

真实场景 :网络请求层几乎必有 try/catch + finally 关 loading + 错误提示,这是每个项目的"标配骨架"。

4.4 串行 vs 并行:看 Promise 何时被创建

知识点(细化)

  • 连续 awaitconst a = await f1(); const b = await f2();)是串行:f2 要等 f1 完成后才发起,总耗时 = 各任务之和。
  • Promise.all([f1(), f2()])并行 :两个 Promise 先创建后等待,同时开始,总耗时 ≈ 最慢者。
  • 判断标准:发起(调用 f())的时刻在 await 之前还是之后。

例子

js 复制代码
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

// 串行:约 3s
await sleep(1000);
await sleep(1000);
await sleep(1000);

// 并行:约 1s
await Promise.all([sleep(1000), sleep(1000), sleep(1000)]);

类比(上菜):串行 = 一道菜吃完再点下一道;并行 = 一次点三盘,厨房同时做。


第 5 段(10min)费曼复述:讲给"零基础同学"听

5.1 复述提纲(出声讲一遍,卡壳处回去重看)

  1. Promise 状态怎么流转? ------ pending → fulfilled / rejected,一次落定不可逆.then 回调最多跑一次。
  2. 链式怎么串起来的? ------ .then 永远返回新 Promise;回调返回 Promise 会被"拍平"------等它完成再继续。
  3. 四个组合方法怎么分? ------ all 全成或快败;allSettled 全收报告不中断;race 先到者胜;any 第一个成功者胜(全败抛 AggregateError)。
  4. async/await 是什么? ------ .then 的语法糖;await 之后是微任务;只暂停当前函数、不阻塞主线程。
  5. 异步错误怎么接? ------ try/catch 就近处理,或调用处 .catch 兜底。

5.2 自测清单(每条能写出例子才算过)

  • 能画出 Promise 状态流转图,并解释"不可逆"保证什么
  • 能说清"拍平"是什么、为什么链式依赖它
  • 能默写 all / allSettled / race / any 的"等谁 + 失败行为"
  • 能解释 await 为什么不阻塞主线程、为什么串行 vs 并行看"创建时机"
  • 能写出 async 函数的 try/catch 兜底模板

第 6 段(20min)练习①:手写 myPromiseAll(code\myPromiseAll.js)

目标:先自己写 ,卡住再看提示。写完用验证清单测。 📄 参考实现已放好:code\myPromiseAll.js(含注释 + 多组测试用例)。

6.1 分步提示(不要一上来就看)

  • 第 1 步 :签名 function myAll(iterable) { return new Promise((resolve, reject) => {...}) }
  • 第 2 步 :把 iterable 转成数组(Array.from),处理空数组resolve([])
  • 第 3 步顺序一致性 是核心难点。用 const results = new Array(len) 按下标存结果------别用 push(push 会把"先完成"的排前面,破坏顺序)。
  • 第 4 步 :用计数器 remaining 统计未完成数,每次成功 --remaining,归零就 resolve(results)
  • 第 5 步 :快速失败------每个子任务 reject 时直接调用外层的 reject(注意普通值要 Promise.resolve(p) 包装)。
  • 第 6 步:对照参考实现,重点看第 3、4 步的写法。

6.2 验证清单(每一条都应通过)

js 复制代码
myAll([1, 2, 3]);                                   // 直接传普通值也能工作
myAll([]).then(v => console.log(v));                // [](空数组)
// 顺序一致:先慢后快的两个任务,结果仍按传入顺序
myAll([sleep(1000), sleep(100)]) → ['慢', '快'];
// 快速失败:含一个 reject 立即 reject,且不等待其他完成
myAll([sleep(1000), Promise.reject('boom')]).catch(...);  // 快速失败: boom

6.3 常见错误排查

报错/现象 原因 修法
结果顺序乱了 用了 push 按完成先后存 results[i] = value 按下标存
空数组永远 pending 没处理 length === 0 开头直接 resolve([])
遇到 reject 不触发整体 reject 没在子任务失败回调里调用外层 reject then(v=>..., reject) 把 reject 传下去
传入的普通值报"不是函数" 忘了 Promise.resolve(p) 包装 每个子项先 Promise.resolve(p)

第 7 段(20min)练习②③:sleep 串并行 + withTimeout

7.1 练习②:sleep 串并行对比(code\sleep.js)

分步提示

  • sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
  • 串行const t1 = Date.now(); await sleep(1000); await sleep(1000); await sleep(1000); 打印耗时(约 3s)。
  • 并行const t2 = Date.now(); await Promise.all([sleep(1000), sleep(1000), sleep(1000)]); 打印耗时(约 1s)。
  • 最后打印差值对比,并在注释里用"创建时机"解释差异。

7.2 练习③:race 超时封装(code\withTimeout.js)

分步提示

  • withTimeout(promise, ms):造一个 timeout Promise,setTimeout 到点 reject(new Error('请求超时'))
  • Promise.race([promise, timeout]) 返回结果。
  • 别忘了 finally(() => clearTimeout(timer)) 清掉定时器(否则即使请求先成功,定时器也会在后台继续跑、白白挂 3 秒)。
  • 用"2 秒的请求 + 1 秒的超时"验证:约 1 秒报"请求超时"。
  • 关键理解 :超时后原请求仍在后台运行 ------race 只是"不再采纳它的结果",并没有真正中止底层请求(要真中止需 AbortController,第 8 段思考题①)。

7.3 验证清单

  • sleep.js:串行约 3s、并行约 1s,注释能解释原因
  • withTimeout.js:2s 请求 + 1s 超时 → 约 1s 报"请求超时"
  • 能说出"为什么超时后原请求还在跑"

第 8 段(10min)思考提高题:先独立想,再看提示

思考题①:Promise.all 快速失败后,剩余 Promise 还在执行吗?能"取消"吗?

提示

  • 在:all 只是"不再采纳"其余结果,任务仍在后台跑。原因:Promise 本身没有"取消"概念,创建了就一定会执行完。
  • 想"失败后取消其他请求":all 做不到。正解是 AbortController------它把信号传给 fetch,才能真正中断底层网络请求。
  • 追问:为什么 JS 不内置取消?因为"取消"涉及"到底谁有权力取消、取消后资源怎么回收",语义复杂;框架(axios、Vue)大多用 AbortController 或 race 模拟"忽略结果"式取消。

思考题②:手写 all 时怎么保证顺序一致?如果某个任务先完成怎么处理?

提示

  • 不能 push------先完成的任务会占前面的位置。要用 results[i] = value 按下标预分配数组
  • 先完成的任务:存进它自己的下标 i 即可,不关心顺序;只有 remaining 归零才整体 resolve。
  • 想想:为什么"先完成"不影响顺序?因为数组位置由下标决定,不是由完成时刻决定。

思考题③:async/await"看着同步",和真正的同步执行本质区别是什么?

提示

  • 真正的同步:主线程阻塞到结束,期间什么都不能做。
  • await:只暂停当前 async 函数,主线程空闲------其他代码、事件、渲染照常。
  • 设计一个"阻塞 vs 不阻塞"的对比:一个用同步 while 睡 3 秒(页面卡死),一个用 await sleep(3000)(页面可交互)------直观感受差异。

第 9 段(10min)测验与收尾

9.1 测验

  • 打开 test\测验.md,限时 15 分钟完成 10 题(巩固 5 + 提升 3 + 横向 2)。
  • 做完再翻答案,把错题按"概念错误 / 实现失误 / 表达不清"分类标记,回到讲义对应章节重读。

9.2 今日能力自检(打分 1--5,低于 3 分明天补)

能力 自评
能画出 Promise 状态机并解释不可逆
能默写四个静态方法的"等谁 + 失败行为"
能徒手写出 myPromiseAll(顺序一致 + 快速失败)
能解释 await 不阻塞主线程、串行 vs 并行
能用 try/catch + finally 写出健壮的异步封装

📌 今日核心速记卡(可截图/摘抄带走)

javascript 复制代码
状态机:pending → fulfilled / rejected,一次落定不可逆(.then 回调最多一次)
executor 是同步执行的(new 的那一刻就跑)

.then 返回新 Promise;回调返回 Promise → 被"拍平",等它落定再继续(链式根基)
finally 无论成败都执行,但透传结果

Promise.all     全部成功 → 按传入顺序返回数组;任一失败 → 立即 reject(不取消其他)
Promise.allSettled 全部落定 → 永不 reject,每项 {status, value/reason}
Promise.race    最先落定者胜(成功失败都算)→ 超时控制
Promise.any     第一个成功者胜;全败才 reject(AggregateError)

async 函数总是返回 Promise;内部 throw → rejected Promise
await = .then 语法糖;await 之后 = 微任务;只暂停当前 async 函数,不阻塞主线程
错误处理:try/catch 就近 + .catch 兜底
串行 vs 并行:看 Promise 何时创建(连续 await 串行,Promise.all 并行)

❌ 常见误区清单(学完自查,哪个说过/想过就划掉)

  • "Promise 可以 resolve 后又 reject" ------ ❌ 状态一次落定不可逆
  • "new Promise 的回调是异步执行的" ------ ❌ executor 是同步执行的
  • "Promise.all 会取消失败的其余请求" ------ ❌ 只忽略结果,任务仍在后台跑
  • "Promise.race 取第一个成功的" ------ ❌ 是第一个"落定"(失败也算)
  • "async 函数返回的是 return 的值" ------ ❌ 返回的是包装后的 Promise
  • "await 会阻塞主线程" ------ ❌ 只暂停当前 async 函数
  • "连续 await 是并行的" ------ ❌ 是串行,并行要用 Promise.all
  • "await 可以写在任意函数里" ------ ❌ 只能 async 函数内 / ES 模块顶层
相关推荐
fail_to_code1 小时前
从 Lighthouse 83 到 100:一次 Vue 项目的性能排查实录
前端·人工智能
用户69371750013841 小时前
DeepSeek 调价正式生效:一夜涨 11 倍,靠低价薅羊毛的日子结束了
前端·人工智能·后端
半仙er1 小时前
第一周01天:this 指向与 call / apply / bind
前端
小帅不太帅1 小时前
给大家推荐一个特别好用的专为 AI Agent 打造的最快浏览器
前端·agent·浏览器
半仙er1 小时前
第一周03天 事件循环与宏任务 / 微任务
前端
做前端的娜娜子1 小时前
#浏览器存储方案:localStorage、sessionStorage 与 Cookie
前端·面试·掘金·金石计划
用户921080262861 小时前
1. Ant Design X Vue 项目介绍:结构、组件和启动方式
前端
摸鱼研究员1 小时前
neverthrow,ts 中优雅的异常处理方案
前端·javascript
jingchao19981 小时前
Cannot read properties of null (reading ‘insertBefore‘)
前端·javascript·vue.js