主题
关键问题总结与async 与 await(Day17 3/3)
本页对应 Day17-Promise,共 5 段视频,约 44 分钟。
学习目标
- 能够针对包含 Promise、setTimeout、async/await 的代码,正确划分同步任务、微任务与宏任务,并写出最终输出顺序。
- 能够判断 Promise 回调进入微队列的时机,解释“状态改变后才满足执行条件”以及未改变状态时为何不执行、不阻塞主线程。
- 能够说明 async 函数中 await 后代码与 Promise.then 回调的等价关系,并判断其作为微任务的执行时机。
- 能够分析多层嵌套 Promise、多个 then/resolve 调用与零延时定时器混合的代码,逐步列出各轮回调入队顺序并预测输出结果。
- 能够用事件循环机制解释“同步任务先执行、微任务清空后再执行宏任务”,以及微任务持续产生时宏任务被延后的现象。
- 能够按“先找同步任务,再确定异步回调入队时机与顺序,最后先微后宏”的步骤复述并校验代码输出。
知识点
先给结论:把 Promise 当“状态机 + 回调队列 + 派生新 Promise”
本节不是背 API,而是建立分析模型:Promise 只有 pending、resolved、rejected 三种状态;状态一旦改变就不可逆;then() 会返回一个新 Promise;多个异步任务按“同步任务 → 微任务 → 宏任务”的队列规则执行。转写稿里反复强调:先找同步任务,再找微任务/宏任务,还要看回调函数什么时候满足执行条件,不能只看它“是不是微任务”。
1. 如何改变 Promise 的状态
为什么需要:只有理解状态怎么改,才能判断回调会不会执行、什么时候执行。
怎么想:Promise 初始是 pending,只有 pending 才能被改成 resolved 或 rejected;已经确定的状态不会再变。
怎么做:resolve(value) 把 pending 改为 resolved;reject(reason) 把 pending 改为 rejected;在执行器里抛出异常,也会把 pending 改为 rejected。
易错点:多次调用 resolve/reject,只有第一次生效;如果 Promise 永远 pending,它对应的 then 回调“这辈子都不会输出”,但也不会阻塞主线程。转写稿里特意用 pending 的例子说明:不改变状态,就不进微队列,主线程不会等它。
2. 一个 Promise 指定多个成功/失败回调,都会调用吗
为什么需要:实际开发中同一个异步结果可能被多处订阅。
怎么想:Promise 实例可以挂多个 then,当实例变为对应状态时,对应的成功或失败回调都会调用。
怎么做:正常写法是先指定回调,再改变状态;也可以先改状态再指定回调,因为状态已经确定,后注册的回调仍能拿到结果。转写稿提到“先改状态再指定回调”的两种来源:执行器里直接 resolve()/reject(),或者延迟更久才调用 then()。
易错点:不要以为“先指定回调”就一定先执行;关键看状态何时改变、回调何时满足执行条件。先指定回调时,状态改变后回调得到数据;先改状态时,指定回调时回调得到数据。
3. then() 返回的新 Promise 结果状态由谁决定
为什么需要:链式调用的核心就是 then() 派生新 Promise。
怎么想:新 Promise 的状态由 then() 里回调函数的执行结果决定,不由原 Promise 直接决定。
怎么做:没写返回值,新 Promise 为 resolved,value 是 undefined;返回非 Promise 的任意值,新 Promise 为 resolved,value 是该值;返回另一个 Promise,新 Promise 跟随它的结果;抛出异常,新 Promise 为 rejected,reason 是异常。
易错点:then 里 return 一个 pending Promise,后续链会一直等;这不是“返回 undefined 继续走”。这也是后面“中断 Promise 链”的基础。
4. 串联、异常传透与中断 Promise 链
为什么需要:多个异步任务要按顺序串起来,并且统一处理异常。
怎么想:then() 返回新 Promise,所以可以写成链式调用;前面任何操作出异常,会跳过中间成功回调,传到最后失败回调处理。
怎么做:用 then() 链串任务,在链尾指定失败回调;要中断链,就在回调里返回一个 pending 状态的 Promise。
易错点:异常传透不是“吞掉异常”,而是沿链寻找失败处理;中断链不是 return undefined,那会让新 Promise 变成 resolved,后续仍会执行。
5. 用转写稿的调试法看输出顺序
为什么需要:面试题常把同步、微任务、宏任务、async/await 混在一起。
怎么想:先找同步任务;再找微任务(then 回调、await 后代码);最后找宏任务(定时器)。同时检查回调是否立即满足执行条件。转写稿里 await 后的代码相当于写在 then 回调里,所以看起来像同步,实际是微任务。
怎么做:例如面试题 01 的分析是:同步输出 3,微任务输出 2、4,宏任务输出 1,所以结果是 3241。类似地,25341、41368275 都要按“同步先走完,再清微任务,再取宏任务”验证。
易错点:微任务不是“创建即执行”,要状态改变后才进队列;宏任务也不是“永远最后”,只是优先级低于当前微任务队列。纠正转写稿同音字:permiss 是 Promise,毁调 是回调,红任务 是宏任务,主塞 是阻塞。
先看结论:async/await 是 Promise 的语法糖
为什么需要:Promise 的链式调用能解决回调地狱,但 .then().then() 写多了仍然不够直观。async 和 await 让我们用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用。
怎么想:async 声明一个函数,函数返回 Promise;await 等待一个 Promise 状态改变,并把“等待之后的代码”放到类似 .then 回调的位置执行。
怎么做:先记住一句话:await 后面的代码不是同步代码,而是微任务;async 函数被调用时,函数体仍会同步执行到第一个 await。
易错点:不要把 async 理解成“整个函数都异步执行”,也不要把 await 后面的 console.log 当成同步输出。
怎么想:async 函数调用是同步开始的
为什么需要:很多执行顺序题错在把 async 函数调用当成了“立刻跳到后面”。实际上,调用 async 函数时,函数体会先同步执行,直到遇到 await 才暂停后续代码。
怎么想:async function FN1() 被调用时,console.log(1) 会先执行;执行到 await FN2() 时,先同步调用 FN2(),所以 FN2 里的 console.log(3) 也会先执行。
怎么做:按转写稿中的 FN1/FN2 面试题整理成下面代码:
js
async function FN1() {
console.log(1);
await FN2();
console.log(2);
}
async function FN2() {
console.log(3);
}
console.log(4);
setTimeout(() => {
console.log(5);
}, 0);
FN1();
new Promise((resolve) => {
console.log(8);
resolve();
}).then(() => {
console.log(7);
});
console.log(6);易错点:FN1() 调用后不是直接输出 2;await FN2() 会先执行 FN2(),输出 3。FN2 没有写 return,它返回的 Promise 状态是成功,所以 await 后面的 console.log(2) 会很快满足执行条件,进入微队列等待。
怎么做:先同步、再微任务、最后宏任务
为什么需要:执行顺序题必须区分同步任务、微任务、宏任务,还要看回调什么时候满足执行条件。转写稿里反复强调:除了区分微队列和宏队列,还要看“什么时候满足执行条件”。
怎么想:先找同步任务。console.log(4)、FN1 中的 console.log(1)、FN2 中的 console.log(3)、new Promise 执行器里的 console.log(8)、最后的 console.log(6) 都是同步输出。
怎么做:同步阶段得到:
text
4 1 3 8 6微任务有 await 后面的 console.log(2),以及 .then 里的 console.log(7)。FN2 返回的成功 Promise 让 2 先进入微队列,new Promise 中 resolve() 后 .then 的 7 进入微队列,所以微任务按进入顺序输出:
text
2 7setTimeout 是宏任务,最后输出 5。最终顺序为:
text
4 1 3 8 6 2 7 5易错点:不要看到 await 就认为 2 立刻同步输出;也不要认为微任务一定“快”,如果它进入微队列晚,仍然会排在后面。转写稿中“红任务”应纠正为“宏任务”,“微阵/微距”应理解为“微队列”。
调试过程:await 后代码相当于 then 回调
为什么需要:理解 await 的本质,才能解释为什么 2 是微任务。转写稿说得很直接:await 后面的代码相当于写在 Promise 对象的 .then 回调里。
怎么想:可以把
js
await FN2();
console.log(2);近似理解为:
js
FN2().then(() => {
console.log(2);
});怎么做:调试时先标同步输出,再标微任务进入时机,最后标宏任务。转写稿中“as think”是 async,“avit/abit”是 await,“permiss”是 Promise,“毁调”是“回调”,“主线成”是“主线程”,“非主塞”是“非阻塞”。
易错点:如果 await 等待的 Promise 永远 pending,它后面的代码不会执行,但不会阻塞主线程。转写稿用“这辈子状态都不会改”说明过这一点:不改变状态就不进入微队列,主线程空下来也不会等它,整体仍然是非阻塞的。
易错点总结
async函数调用本身是同步开始的,不是整个函数异步执行。await后面的代码是微任务,类似.then回调。- Promise 构造器里的执行器函数是同步执行的,所以
console.log(8)先输出。 .then回调是微任务,setTimeout是宏任务。- 微任务也要看进入队列的时机,不是所有微任务都“天然靠前”。
- 永远 pending 的 Promise 不会阻塞主线程,只是它后面的回调不执行。
先分清同步任务和异步任务
为什么需要:JS 是单线程执行,如果所有回调都立刻排队执行,就会阻塞后续代码。把任务分成同步和异步,才能做到“非阻塞”。怎么想:拿到一段代码,不要先纠结微任务宏任务,先找同步任务——第一次执行时立即执行的语句。怎么做:同步任务先跑完;异步任务的回调进入不同队列等待。转写稿里反复强调“先找同步任务”,比如 Promise 的执行器函数、async 函数调用本身,都是同步执行的。易错点:不要看到回调就认定是异步;new Promise(...) 的执行器是同步的,async 函数在遇到 await 之前也是同步执行的。
微队列和宏队列分别装什么
为什么需要:异步回调的优先级不同,必须分队列管理。怎么想:看回调来源。微队列用来保存待执行的微任务回调,比如 Promise 的回调、MutationObserver 的回调;宏队列按最新 W3C 标准分为交互队列(DOM 事件回调)和延迟队列(定时器回调、Ajax 回调)。怎么做:面试题里常见的是定时器进入延迟队列,Promise 的 then 回调进入微队列。执行优先级是:微队列 > 交互队列 > 延迟队列。易错点:不要把“宏任务”只理解成定时器;标准里宏队列还分交互队列和延迟队列,只是很多题里出现的宏任务都是定时器,所以表现为“微任务先于宏任务”。
执行顺序规则:同步先完,微任务清空,再取宏任务
为什么需要:只有统一规则,才能预测输出顺序。怎么想:JS 引擎首先必须执行所有初始化同步任务代码;每次准备取出第一个宏任务执行前,都要把微队列里的微任务一个一个取出来执行。怎么做:先同步,再看微队列,微队列清空后才取宏任务。易错点:微任务执行过程中如果又产生新的微任务,也要在本轮继续清空;宏任务不能插队到未清空的微队列前面。
执行时机比任务类型更关键:面试题 01 的 3241
为什么需要:只知道“微任务优先”不够,还要看回调什么时候满足执行条件。怎么想:面试题 01 中,输出 3 是同步任务;输出 2 和 4 是 Promise 回调,因为 Promise 创建之初状态就确定了,回调立即满足条件,进入微队列;输出 1 是定时器,进入延迟队列。怎么做:同步先输出 3,然后微队列输出 2、4,最后宏任务输出 1,所以答案是 3241。转写稿还做了一个调试:在输出 4 后面套一个状态永远不改变的 Promise,结果 4 不输出,也不会阻塞程序。这说明“你是微任务”不等于“你一定很快执行”,状态不改变就进不了微队列。易错点:以为微任务一定先执行;实际上它得先满足条件进入队列,程序主打非阻塞,没有人会一直等一个不改变状态的 Promise。
面试题 02、03:谁先改状态,谁先进微队列
面试题 02 的输出是 25341:2、5 是同步;3、4 是 then 回调,状态成功,进入微队列;1 是定时器宏任务。易错点是漏掉 then 返回的新 Promise 也会让后续回调满足条件。
面试题 03 更绕:first 是箭头函数,返回一个 Promise;同步先输出 3、7、4。里面有两个 resolve,但只有第一个生效,所以先输出 1,再输出 2;first 返回的 Promise 要到后面才改变状态,因此 1 比 2 先进入微队列;定时器里的 5 最后输出。怎么想:不要按代码位置硬排,要看“谁的状态先改变,谁先进入微队列”。易错点:两个 resolve 都以为会执行;忽略箭头函数省略 return 时,表达式整体就是返回值。
多轮微任务:面试题 04 的 238456 和最后的 0
为什么需要:微任务不是一次性全部进队列,有些要等前面的回调执行时才满足条件。怎么想:同步任务执行过程中,第一轮进入微队列的是 28 到 38 行的函数和输出 8 的回调,所以先输出 2、3、8;执行 2 时又 new Promise 输出 3,并让 34 到 36 行的回调进入微队列,输出 4;38 到 40 行的函数执行结束后,6 进入微队列;5 要等 4 执行完才进入,最后输出 5。微队列清空后,才轮到定时器的 0。怎么做:按“第几轮进入微队列”画图,不要只按代码上下顺序排。易错点:把 5 提前;忽略微任务执行中还会产生新微任务。
async/await 的陷阱:面试题 05 的 41368275
为什么需要:await 后面的代码看起来像同步,其实相当于放进 Promise 的 then 回调里。怎么想:FN2 返回 Promise,状态立即成功,所以 await 后面的输出 2 是微任务;同步任务先输出 4、1、3、8、6;微任务输出 2、7;定时器 5 最后。怎么做:把 await 后代码视为微任务,但 async 函数调用本身仍同步执行到 await。易错点:把 await 后代码当同步;忘记 FN2 没写 return 时返回的 Promise 状态仍是成功。
易错点收束
判断顺序时固定四步:先找同步任务;再找微任务和宏任务来源;再看回调是否立即满足执行条件;最后看微任务是否分多轮进入。记住:微队列 > 交互队列 > 延迟队列;每次取宏任务前清空微队列;状态不改变的回调不会进队列,也不会阻塞主线程。
课堂画面





代码示例
示例 1:同步任务、微任务、宏任务的执行顺序
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Promise 微任务 / 宏任务执行顺序</title>
</head>
<body>
<script>
console.log(3); // ① 同步任务
Promise.resolve().then(() => console.log(2)); // ② 微任务(状态已确定)
setTimeout(() => console.log(1)); // ③ 宏任务(延迟队列)
Promise.resolve().then(() => console.log(4)); // ④ 微任务(状态已确定)
// 永远 pending 的 Promise:then 回调永远进不了微队列
const p = new Promise((resolve, reject) => {
// 既不 resolve 也不 reject,状态一直 pending
});
p.then(() => console.log('永远不会输出'));
</script>
</body>
</html>输出结果:3 2 4 1
关键行与易错点:
console.log(3)是最外层的同步代码,JS 引擎首先执行全部同步任务,所以 3 最先输出。Promise.resolve()在创建的那一刻状态就已经是 resolved,因此它的.then回调立即满足执行条件,进入微队列;两个微任务按注册顺序排队,得到 2、4。setTimeout的回调进入延迟队列(宏任务),必须等所有微任务清空后才执行,所以 1 最后输出。- 执行优先级是:微队列 > 交互队列 > 延迟队列,并且每次取第一个宏任务前,都要把微队列一个一个取完。
- 易错点:
const p = new Promise(() => {})这个实例状态永远是pending,它的then回调根本不会进入微队列,所以永远不会输出;但它也不会阻塞主线程——JS 是"非阻塞"的,不会等一个永远不改变状态的 Promise。
示例 2:一个 Promise 实例指定多个回调 + 先改状态再指定回调
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>一个 Promise 多个回调</title>
</head>
<body>
<script>
const p = new Promise((resolve, reject) => {
console.log(2); // ① 执行器函数是同步执行的
resolve('ok'); // ② 立即把状态改为 resolved
setTimeout(() => console.log(1)); // ③ 宏任务,进延迟队列
});
p.then(() => console.log(3)); // ④ 微任务
p.then(() => console.log(4)); // ⑤ 微任务
console.log(5); // ⑥ 同步任务
</script>
</body>
</html>输出结果:2 5 3 4 1
关键行与易错点:
new Promise(executor)里的执行器函数是同步执行的,所以 2 在同步阶段就输出了,不要误以为是异步。resolve('ok')把状态从pending改为resolved;同一个 Promise 实例上注册的多个成功回调都会被调用(3 和 4 都会执行),并按注册顺序进入微队列。console.log(5)属于同步任务,同步代码优先于所有微任务,所以 5 排在 3、4 之前。- 先改状态再指定回调也没问题:本例中
resolve先执行、then后注册,回调依然会被调用;反过来"先指定回调、后改状态"同样成立——只要状态变了,回调就会执行并拿到数据。 - 易错点:定时器写在执行器里,但
console.log(1)的回调是宏任务,哪怕延迟是 0,也要等微任务 3、4 全部清空后才执行,所以 1 一定在最后。
示例 3:async / await 与事件循环
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>async / await 执行顺序</title>
</head>
<body>
<script>
async function fn2() {
console.log(3); // ① 同步执行(调用 fn2 时立即执行)
}
async function fn1() {
console.log(1); // ② 同步执行
await fn2(); // ③ await 后面的代码相当于 then 回调
console.log(2); // ④ 微任务!
}
console.log(4); // ⑤ 同步任务,最先输出
fn1(); // ⑥ 调用 async 函数,同步执行到 await
console.log(8); // ⑦ 同步任务
new Promise((resolve) => {
console.log(6); // ⑧ 执行器同步执行
resolve();
}).then(() => console.log(7)); // ⑨ 微任务
setTimeout(() => console.log(5)); // ⑩ 宏任务
// 最终输出:4 1 3 8 6 2 7 5
</script>
</body>
</html>输出结果:4 1 3 8 6 2 7 5
关键行与易错点:
async函数被调用时,函数体是同步执行的,一直执行到第一个await才挂起,所以 1、3 都在同步阶段输出。await后面的代码相当于写在该 Promise 的.then回调里,是微任务。所以console.log(2)虽然看起来像同步代码,实际上要等所有同步任务跑完才输出。fn2没有return,返回undefined,await得到的是非 Promise 值——但await依然会让出线程,后续代码照样进微队列。- 微任务的排队顺序:
2在await挂起时就已入队,7在new Promise(...).then时才入队,所以 2 先于 7。 - 易错点:
console.log(8)和new Promise执行器里的console.log(6)都是同步代码,会先于微任务 2、7 输出;setTimeout的 5 是宏任务,必须等微队列完全清空后才执行,所以永远排在最后。
常见错误
现象: 把
new Promise(executor)里的执行器当成异步代码,先排微任务。
原因: 执行器是同步执行的,只有then/catch/finally注册的回调才按队列调度。
修法: 分析输出顺序时先标同步代码,再单独标回调入队时机。现象: 看到
then就认为一定最后执行,或看到setTimeout就认为一定晚于所有 Promise。
原因: 只看任务类型,没看 Promise 状态何时改变、回调何时满足入队条件。
修法: 先判断状态改变时机,再判断回调进入微队列/宏队列的轮次。现象:
then链中后续拿到undefined,或后续任务莫名不执行。
原因: 上一个回调漏写return,或返回了 pending Promise。
修法: 明确每个then的返回值;需要中断链时显式返回 pending Promise,不要误写return undefined。现象: 链式调用中异常没有被捕获,或中间
catch后后续逻辑状态混乱。
原因: 未利用异常穿透在末尾统一处理,或局部捕获后没有恢复链状态。
修法: 默认在链末尾加catch/finally;只有需要局部兜底时才在中间catch并明确返回值。现象: 把
await后面的代码当同步代码分析。
原因:await后续逻辑相当于进入 Promise 回调续体,通常按微任务调度。
修法: 以await为分界分段分析,先执行await前同步部分,再排后续微任务。
练习
写出输出顺序:
jsnew Promise((resolve) => { console.log(1); resolve(2); }).then((v) => console.log(v)); setTimeout(() => console.log(3), 0); console.log(4);写出输出顺序并说明每个
then的结果来源:jsPromise.resolve(1) .then(() => { console.log(2); }) .then((v) => console.log(v)) .then(() => { throw new Error('x'); }) .catch((e) => console.log(e.message)) .finally(() => console.log('f'));分析下面代码为什么
ok不输出,并给出一种“可控地中断 Promise 链”的改法:jsasync function f() { const p = new Promise(() => {}); await p; console.log('ok'); } f();写出输出顺序:
jsasync function async1() { console.log('A'); await async2(); console.log('B'); } async function async2() { console.log('C'); } console.log('D'); setTimeout(() => console.log('E'), 0); async1(); Promise.resolve().then(() => console.log('F')); console.log('G');简答:在
then链中间停止后续回调,且不进入失败回调,应返回什么?写出关键代码。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 面试题01 | 05:10 |
| 2 | 面试题02 | 04:16 |
| 3 | 面试题03 | 13:00 |
| 4 | 面试题04 | 14:53 |
| 5 | 面试题05 | 07:15 |
小结
- 分析异步输出顺序的固定流程是:先同步,再异步;异步中再分微任务、宏任务,并检查回调入队时机。
executor同步执行,then/catch/finally回调按状态改变和队列规则调度,不能只凭书写位置判断。then链的新 Promise 状态由回调的返回值、返回 Promise、抛错共同决定,漏return是高发问题。- 异常穿透适合末尾统一
catch;需要中断链时应返回 pending Promise,而不是普通值。 async/await分析要以await为分界,await后续代码不能按同步代码排。- 微任务会优先清空,且执行中可继续产生微任务;宏任务要等微队列空后才取。
- 面试题建议按“标同步→列回调→判状态改变时机→判入队轮次→按优先级输出”作答,减少凭感觉排序。