主题
Event Loop 事件循环
本页对应 1-12Eventloop,共 5 段视频,约 49 分钟。
学习目标
- 能用自己的话解释 JavaScript 单线程、事件循环、调用栈、宿主环境与任务队列的作用,并说明定时器等异步任务为何不会阻塞后续同步代码。
- 能根据“调用栈—宿主环境—任务队列”模型,逐步分析包含同步代码、
setTimeout、事件回调的代码,并准确预测输出顺序。 - 能区分宏任务与微任务,列举常见宏任务(如脚本解析、定时器、请求完成事件、用户交互事件)和微任务(如
Promise.then/catch回调),并说明new Promise执行器是同步执行、then/catch回调是异步执行。 - 能按照“执行同步代码→清空微任务队列→执行下一个宏任务”的规则,分析宏任务与微任务穿插的复杂代码及经典面试题,并写出正确输出顺序。
- 能说明
Promise.all的语法与返回值,使用它管理多个 Promise,并在then中按传入顺序获取全部成功结果组成的数组,在catch中获取第一个失败原因。 - 能使用
Promise.all结合接口请求完成“获取多个城市天气并渲染页面”的案例,并构造一个失败请求验证任一 Promise 失败即进入catch。
知识点
事件循环到底解决什么问题
为什么需要:JavaScript 是单线程的,某一刻只能执行一行代码。如果遇到定时器、请求这类耗时操作就原地等待,后面的代码就会被阻塞。为了让耗时代码不阻塞其他代码运行,JS 设计了事件循环模型。
怎么想:同步代码直接执行;异步代码交给宿主环境(前端主要是浏览器)处理,等有了结果,再把回调函数放进任务队列排队;调用栈空闲后,事件循环反复从任务队列里取回调函数执行。
怎么做:先看笔记中的代码:
javascript
console.log(1)
setTimeout(() => {
console.log(2)
}, 2000)
console.log(3)执行时,console.log(1) 是同步代码,立即输出 1;setTimeout 是异步代码,交给浏览器等待 2 秒,但不会阻塞后续解析;console.log(3) 继续执行,输出 3;等 2 秒到了,回调函数进入任务队列,调用栈空闲后被取出执行,最后输出 2。所以顺序是 1、3、2。
易错点:不要把 setTimeout 的延迟时间理解成“主线程停在那里等”。它只是把回调交给宿主环境,时间到了再排队,并不是立刻插队执行。
从调用栈、宿主环境、任务队列看执行过程
为什么需要:只记住“异步后执行”不够,遇到复杂代码容易判断错。事件循环模型主要看三部分:调用栈(执行栈)、宿主环境、任务队列。
怎么想:调用栈负责执行同步代码;宿主环境负责等待异步时机,例如浏览器处理定时器;任务队列存放等待执行的回调函数。关键条件是:调用栈空闲后,事件循环才去任务队列里取回调。
怎么做:结合上面的代码逐步走一遍:console.log(1) 进调用栈,输出 1 后出栈;setTimeout 交给浏览器;console.log(3) 进调用栈,输出 3 后出栈;同步代码执行完,调用栈空闲;2 秒后回调进入任务队列;事件循环把回调拿到调用栈执行,输出 2。

易错点:回调进入任务队列只是“排队等待”,不是“立刻执行”;只有调用栈空闲时才会被取出。事件循环的定义可以记成:执行代码和收集异步任务,在调用栈空闲时,反复调用任务队列里回调函数的执行机制。
0 秒定时器为什么仍然后打印
为什么需要:这是事件循环里最容易混淆的点。很多同学看到 setTimeout(..., 0) 就以为不等待,应该马上执行。
怎么想:0 秒只表示“尽快把回调送入任务队列”,并不表示它能插到同步代码前面。同步代码没执行完,调用栈没空闲,回调仍然要排队。
怎么做:用转写稿中的验证例子:
javascript
console.log(4)
setTimeout(() => {
console.log(5)
}, 0)
console.log(6)分析顺序:先执行同步 console.log(4),输出 4;setTimeout 是异步,交给浏览器,0 秒后回调进入任务队列等待;继续执行 console.log(6),输出 6;同步代码结束、调用栈空闲后,才取回调执行,输出 5。结果是 4、6、5。转写稿中在 VS Code 保存后运行,控制台结果也是 465,说明分析正确。
易错点:0 秒定时器仍然是异步任务;不要按代码书写位置直接排成 4、5、6。判断时要看“同步代码是否执行完、调用栈是否空闲”。
请求完成事件和点击事件也要排队
为什么需要:实际异步不只有定时器,请求完成事件、用户点击事件也遵循事件循环。不会判断它们,就容易把复杂代码顺序看错。
怎么想:请求的 loadend 回调、点击事件回调都不是声明后立刻执行。请求要等响应回来,点击要等用户真的点击;触发后回调才进入任务队列,再等调用栈空闲。
怎么做:转写稿中的练习代码包含同步输出、两个 0 秒定时器、请求完成事件、点击事件绑定和最后的同步输出。分析时先执行同步代码,输出 1、6;两个 0 秒定时器回调先排队,按进入队列顺序输出 2、3;请求响应回来后,loadend 回调排队并输出 4;点击事件未点击不会执行。浏览器控制台先看到 1、6、2、3、4,点击页面后输出 5。转写稿中按 F12 打开控制台验证,结果与推导一致。
易错点:请求完成事件和点击事件不会因为代码写到那里就执行;不点击就不会输出 5。任务队列通常按进入顺序取回调,先进入的先执行。
小结:分析事件循环的固定步骤
为什么需要:面试题和调试中代码会混着同步、定时器、请求、事件,必须有固定分析顺序。
怎么想:先找同步代码,再找异步代码交给谁,接着看回调进入哪个队列,最后等调用栈空闲后取队列任务。
怎么做:可以按四步走:第一,同步代码直接执行;第二,异步代码交给宿主浏览器环境;第三,异步有结果后把回调放入任务队列排队;第四,调用栈空闲后反复调用任务队列里的回调。后续学习宏任务和微任务时,还要记住微任务优先级更高:调用栈空闲后先清空微任务队列,再执行下一个宏任务。
易错点:不要一看到回调就立刻执行;不要把“延迟时间短”当成“优先级高”;分析时最好画出调用栈、宿主环境、任务队列,再按步骤推导,最后用浏览器控制台运行验证。
练习目标:先看模型再读代码
为什么需要:事件循环练习的目的不是背结论,而是用“调用栈、宿主环境、任务队列”这套模型,解释一份包含同步代码、定时器、请求回调和点击事件的代码为什么按某个顺序输出。
怎么想:先把代码逐行分成同步任务和异步任务;同步任务直接进调用栈,异步任务先交给宿主环境,等时机成熟再进任务队列排队。
怎么做:对照本节笔记中的模型图阅读代码,边读边标记“立即执行”和“等待执行”的语句。

易错点:不要一看到 setTimeout(..., 0) 就认为回调会立刻执行;也不要忽略函数声明、事件绑定本身并不会马上触发回调。
第一轮:同步代码先入调用栈
为什么需要:事件循环的第一条规则是同步代码优先执行,否则就无法解释为什么后面的输出会排在定时器前面。
怎么想:从上往下解析时,遇到普通输出就是同步任务;遇到函数声明只是声明,不会执行函数体;遇到事件绑定也只是登记,不会立刻触发。
怎么做:转写稿中的练习代码开头有 console.log(1),它是同步任务,直接进调用栈并打印 1;继续往下解析,函数声明先跳过;最后 console.log(6) 也是同步任务,调用栈执行后打印 6。因此第一轮同步阶段先得到 1、6。
易错点:把函数声明当成函数调用;把“绑定点击事件”当成“点击已经发生”;忘记同步代码没有执行完时,任务队列里的回调不会提前插队。
第二轮:异步任务进入宿主环境并在队列等待
为什么需要:定时器、请求完成事件、用户点击事件都不能阻塞后续代码,所以要先交给宿主环境处理,再进入任务队列等待。
怎么想:练习中有两个延迟为 0 的定时器。0 只表示等待时间很短,不表示同步立即执行;它们仍然属于异步任务。
怎么做:两个定时器先交给浏览器这个宿主环境,时间到后把回调推入任务队列。等调用栈空闲后,任务队列按先入先出执行,所以先打印第一个定时器里的 2,再打印第二个定时器里的 3。
易错点:把 0 秒理解成“马上插到当前代码中间执行”;忽略任务队列的排队顺序;以为调用栈还在忙时任务队列会强行执行。
第三轮:请求完成事件与用户点击事件
为什么需要:真实代码里不只有定时器,还有网络请求完成和用户交互,它们同样要通过事件循环排队。
怎么想:调用请求函数后,内部会实例化对象、设置请求地址、监听 load end 事件并发请求;但 load end 回调要等服务器响应回来才触发。点击事件则要等用户真正点击页面才触发。
怎么做:调用函数时,请求相关代码进入调用栈执行,load end 回调先进入宿主环境等待响应;点击事件绑定后也进入宿主环境等待用户操作。服务器响应回来后,load end 回调被推入任务队列,调用栈空闲时执行并打印 4;用户点击后,点击回调进入任务队列,再执行并打印 5。
易错点:以为发了请求就立刻执行 load end;以为绑定了点击事件就会自动打印 5;忽略“点击不点就不执行”。
验证与小结:用控制台确认执行顺序
为什么需要:只靠口算容易漏掉“调用栈是否空闲”“回调是否已进入队列”,运行验证能检查推导。
怎么想:先写出同步阶段的 1、6,再写任务队列中先到的 2、3,响应回来后是 4,点击后才是 5。
怎么做:在浏览器中打开开发者工具控制台,运行练习代码,先看到 1 6 2 3 4;点击页面后出现 5。这与转写稿中的验证过程一致。
易错点:看到结果 16234 就以为结束,忘记点击事件尚未触发;分析时把“等待执行”写成“立即执行”;不区分调用栈、宿主环境和任务队列的职责。
为什么需要区分宏任务与微任务
为什么需要:JavaScript 是单线程的,代码默认逐行执行;如果遇到定时器、请求等耗时操作就原地等待,后续代码会被阻塞。事件循环就是为避免这种阻塞而设计的执行机制。ES6 之后引入 Promise,让 JS 引擎也可以发起异步任务,于是异步任务需要进一步区分来源和优先级。
怎么想:先判断代码是同步还是异步;如果是异步,再判断它由浏览器环境执行,还是由 JS 引擎环境执行。前者归宏任务,后者归微任务。
怎么做:记住两类代表:宏任务包括 JS 执行脚本事件、setTimeout/setInterval、AJAX 请求完成事件、用户交互事件等;微任务目前常见的是 Promise.then() 的回调。转写稿里的“红任务”应纠正为“宏任务”,“文人务”应纠正为“微任务”。
易错点:new Promise((resolve) => { ... }) 传入的回调函数是同步执行的,真正异步的是 then/catch 里的回调。
宏任务与微任务怎么划分
为什么需要:只把异步任务都放进一个队列,无法解释 Promise 与定时器混用时的输出顺序。
怎么想:把任务队列看成两类:宏任务队列、微任务队列。不同任务进入不同队列,进入队列后是“等待调用”,不是立刻执行。
怎么做:结合下图记忆划分。

易错点:setTimeout(..., 0) 仍然是宏任务,仍然要排队;用户点击事件不点击就不会执行,AJAX 完成事件也要等响应回来。
事件循环模型如何运转
为什么需要:需要一套稳定模型解释“同步先执行、异步后回调”的顺序。
怎么想:模型包含调用栈、宿主环境、任务队列。同步代码进调用栈执行;异步代码交给宿主环境,时机成熟后把回调推入对应队列;调用栈空闲时再取队列任务。
怎么做:看下图。

基本流程是:执行第一个 script 脚本事件宏任务中的同步代码;遇到宏任务/微任务交给宿主环境,有结果回调函数进入对应队列;当执行栈空闲时,清空微任务队列,再执行下一个宏任务,如此循环。

易错点:调用栈空闲后不是马上执行所有队列,而是先清空微任务队列,再执行下一个宏任务。
用代码推导执行顺序
为什么需要:只看概念容易混淆,必须用代码验证“0 秒定时器也不立即执行”。
怎么想:同步先打印;定时器交给浏览器,0 秒也进宏任务队列等待;同步代码执行完后,调用栈空闲,再取宏任务。
怎么做:课堂示例可简化为:
js
console.log(4);
setTimeout(() => {
console.log(5);
}, 0);
console.log(6);推导:先执行同步 4,定时器回调进入宏任务队列,再执行同步 6;调用栈空闲后执行回调 5。控制台验证结果为 4 6 5。
易错点:不要把“延迟 0 秒”理解成“立刻插队执行”。
微任务优先的完整流程
为什么需要:Promise 出现后,必须知道微任务优先级高于下一个宏任务。
怎么想:同步代码先走;new Promise 内部同步执行;then 回调进微任务队列;定时器回调进宏任务队列;调用栈空闲后先清空微任务队列,再执行宏任务。
怎么做:
js
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
new Promise((resolve) => {
console.log(3);
resolve(4);
}).then((res) => {
console.log(res);
});
new Promise((resolve) => {
console.log(5);
resolve(6);
}).then((res) => {
console.log(res);
});
console.log(7);推导:同步输出 1、3、5、7;微任务队列有 4、6;宏任务队列有 2。调用栈空闲后清空微任务,输出 4、6,再执行宏任务输出 2。结果为 1 3 5 7 4 6 2,转写稿中在 VS Code 保存后打开浏览器控制台验证一致。
易错点:微任务不是执行一个就停,而是一次清空;then 回调才是微任务。
经典面试题巩固
为什么需要:面试题常把宏任务、微任务穿插,考察是否真正理解事件循环。
怎么想:先找同步代码,再按队列归类;注意定时器内部还可能再产生微任务。
怎么做:
js
console.log(1);
setTimeout(() => {
console.log(2);
new Promise((resolve) => {
resolve(3);
}).then((res) => {
console.log(res);
});
}, 0);
new Promise((resolve) => {
setTimeout(() => {
console.log(4);
}, 0);
resolve(5);
}).then((res) => {
console.log(res);
});
new Promise((resolve) => {
resolve(6);
}).then((res) => {
console.log(res);
});
console.log(7);推导:同步输出 1、7;微任务输出 5、6;第一个宏任务输出 2,其内部 then 产生微任务 3,清空后输出 3;再执行下一个宏任务输出 4。最终 1 7 5 6 2 3 4,控制台验证为 1756234。
易错点:宏任务执行中产生的新微任务,仍要在当前宏任务之后、下一个宏任务之前清空;同队列任务按进入顺序执行。
这道题要练什么
事件循环的经典面试题,重点不是背结论,而是练“用模型推顺序”的能力。为什么需要它?因为实际开发中定时器、Promise、事件回调经常混在一起,只靠肉眼从上往下读,很容易把异步代码当成同步代码。怎么想?先把代码拆成同步任务、宏任务、微任务三类,再按“同步先执行 → 清空微任务 → 执行下一个宏任务”的顺序推。怎么做?下面用配套代码完整走一遍。易错点是:看到 setTimeout(..., 0) 就以为它马上执行,看到 new Promise(...) 就以为整段都是异步。
先看代码:把题目摆出来
请切换到对应配套代码,查看具体代码,并回答打印顺序:

结合截图和讲解,代码可以整理成下面这样:
js
console.log(1);
setTimeout(() => {
console.log(2);
new Promise((resolve) => {
resolve(3);
}).then((res) => {
console.log(res);
});
}, 0);
new Promise((resolve) => {
setTimeout(() => {
console.log(4);
}, 0);
resolve(5);
}).then((res) => {
console.log(res);
});
new Promise((resolve) => {
resolve(6);
}).then((res) => {
console.log(res);
});
console.log(7);先别急着运行,按事件循环模型推一遍。
分析前的准备:三个角色和两条队列
为什么需要先摆模型?因为这道题的难点是“宏任务和微任务穿插”,没有模型就只能靠猜。怎么想?记住三个角色:调用栈、宿主环境(浏览器)、任务队列;任务队列又分宏任务队列和微任务队列。怎么做?按下面的规则推:
- 同步代码直接进调用栈,执行完出栈;
setTimeout是宏任务,先交给浏览器,时间到了把回调放进宏任务队列;new Promise里的执行器是同步的,会立刻执行;.then里的回调是微任务,进微任务队列;- 调用栈空闲后,先清空微任务队列,再取下一个宏任务。
易错点:setTimeout 的 0 秒不代表立刻执行,它只是“尽快排到宏任务队列”,仍然要等同步代码和微任务跑完。
逐步推导打印顺序
开始解析第一个宏任务:<script> 脚本本身。
第一行 console.log(1) 是同步代码,进调用栈执行,打印 1,出栈。
接着遇到第一个 setTimeout,它是宏任务,回调交给浏览器,时间到后进入宏任务队列,等待执行。
继续往下,遇到 new Promise((resolve) => { ... })。注意:Promise 的执行器是同步的,所以这段会立刻进调用栈。执行器内部先遇到 setTimeout(() => { console.log(4); }, 0),这是宏任务,交给浏览器,之后进入宏任务队列。接着执行 resolve(5),Promise 状态变为成功。但 .then 的回调是微任务,不会立刻执行,而是进入微任务队列,等待打印 5。
继续遇到第二个 new Promise,执行器同步执行 resolve(6),.then 回调进入微任务队列,等待打印 6。
最后一行 console.log(7) 是同步代码,立刻打印 7。
此时同步代码执行完,调用栈空闲。当前输出是 1 7。
接下来清空微任务队列:先执行第一个 .then,打印 5;再执行第二个 .then,打印 6。微任务队列清空。
然后取第一个宏任务,也就是最外层 setTimeout 的回调。执行 console.log(2),打印 2。紧接着回调里又创建了一个 Promise,resolve(3) 后 .then 回调进入微任务队列。这个宏任务执行完后,调用栈再次空闲,先清空微任务队列,打印 3。
最后取第二个宏任务,也就是 Promise 执行器里的 setTimeout 回调,执行 console.log(4),打印 4。
所以完整顺序是:
text
1 7 5 6 2 3 4实际运行验证
讲解中把代码放到 VS Code 里运行,控制台输出正是 1756234,和推导一致。这一步为什么需要?因为事件循环题最容易“自认为推对了”,实际跑一遍能验证模型有没有用错。怎么做?保存代码,打开控制台看输出;如果有偏差,就回到模型检查:是不是把 .then 当同步了,是不是把 setTimeout 的 0 秒当成立即执行了。易错点:不要只记答案,要能复述“为什么 2 在 5、6 后面”。
易错点集中提醒
第一,new Promise 的执行器是同步的,但 .then 回调是微任务,不能把整段 Promise 都当成异步。第二,setTimeout 即使延迟 0 秒,也是宏任务,必须排在同步代码和微任务之后。第三,微任务不是“执行一个就停”,而是每次调用栈空闲后要清空整个微任务队列。第四,宏任务内部如果又产生微任务,要等这个宏任务执行完,再清空新产生的微任务,然后才进入下一个宏任务。
小结
这道题的核心就是一句话:同步代码先跑完,微任务清空,再按顺序执行宏任务;宏任务内部产生的新微任务,仍然要在下一个宏任务之前清空。只要按“调用栈 → 微任务队列 → 宏任务队列”的模型一步步推,再复杂的面试题也能拆开。
目标与使用场景
为什么需要:实际开发中经常要同时请求多个接口,比如同时请求“北京”“上海”“广州”“深圳”的天气。每个请求都会返回一个 Promise,如果逐个写 then,代码会重复,也不方便在所有结果都回来后再统一渲染。
怎么想:把多个 Promise 合并成一个新的 Promise,用这个新 Promise 统一管理这一组异步任务,再通过 then 或 catch 获取结果。
怎么做:使用静态方法 Promise.all。
易错点:Promise.all 是合并“已经生成的 Promise”,不是替你发请求;请求仍要先通过 axios 等函数生成 Promise。

概念与返回值
为什么需要:多个 Promise 的结果需要一次性拿到,避免分散处理。
怎么想:Promise.all 接收一个数组,数组里放多个 Promise 对象,返回一个新的 Promise。
怎么做:调用后得到新 Promise,再对它使用 then、catch。
易错点:不要把 Promise.all 写成“Promise 点 O”;正确拼写是 Promise.all。
语法与 then/catch 触发时机
为什么需要:需要明确成功和失败分别什么时候触发。
怎么想:then 要等所有 Promise 都成功;catch 只要任意一个 Promise 失败就触发。
怎么做:保留笔记中的语法:
javascript
const p = Promise.all([Promise对象1, Promise对象2...])
p.then(results => {
// results: 结果数组[Promise对象1的结果,Promise对象2的结果]
}).catch(err => {
// err: 第一个失败的Promise抛出的异常
})易错点:不是“所有 Promise 都失败”才进 catch,而是任意一个失败就进;then 的 results 是数组,顺序与传入的 Promise 对应;catch 的 err 是第一个失败原因,不是数组。
需求:同时请求四个城市天气
为什么需要:四个城市天气都拿到后再渲染页面,页面展示更完整。
怎么想:先用 axios 分别得到 p1、p2、p3、p4 四个 Promise,再用 Promise.all([p1, p2, p3, p4]) 合并。
怎么做:在 then 中拿到结果数组,用 map 生成 li,再用 join('') 拼成字符串,最后通过 innerHTML 渲染到 ul 中。
易错点:接口参数名是 city,位置在 params 查询参数中;城市编码按接口文档填写,不要随手写。

核心实现可写成:
javascript
const p1 = axios.get('接口地址', { params: { city: '北京编码' } })
const p2 = axios.get('接口地址', { params: { city: '上海编码' } })
const p3 = axios.get('接口地址', { params: { city: '广州编码' } })
const p4 = axios.get('接口地址', { params: { city: '深圳编码' } })
const p = Promise.all([p1, p2, p3, p4])
p.then(res => {
const html = res.map(v => {
const { area, weather } = v.data.data
return `<li>${area} - ${weather}</li>`
}).join('')
document.querySelector('.weather').innerHTML = html
}).catch(err => {
console.dir(err)
})调试过程与常见错误
转写稿中先只写 p1 测试 Promise.all 的返回值,console.log 得到的是一个 Promise;继续 .then 后打印 res,发现它是数组,数组项结构与直接调用 axios 后的返回值一致,所以取数据要按 res[0].data.data.area、res[0].data.data.weather 这样的层级来取。随后把 p2、p3、p4 加进数组,then 中就能拿到四个城市结果。渲染时用 map 映射成 li,再 join(''),最后赋值给 ul 的 innerHTML。调试中曾把变量名 area 误写成 AREAT,控制台报错,改正后正常。最后故意把最后一个 URL 改错,测试 catch:因为任意一个失败,then 不触发,页面没有数据,控制台能看到 404 Not Found,说明失败会进入 catch。
易错点:then 中 res 是数组;catch 中 err 是单个失败原因;注意 innerHTML、console、params 等拼写。
小结
Promise.all 适合“合并多个 Promise,并等待所有同时成功的结果”的场景;如果有一个报错,最终会进入失败状态。当需要同时请求多个接口,并把多个接口数据同时渲染到网页上时,就可以使用它。
课件图示




代码示例
示例一:事件循环基础(同步代码不等待定时器)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>事件循环基础</title>
</head>
<body>
<!-- 打开浏览器控制台(F12)查看打印顺序 -->
<script>
console.log(1)
setTimeout(() => {
console.log(2)
}, 2000)
console.log(3)
</script>
</body>
</html>要点解释与易错点:
console.log(1)是同步代码,直接进调用栈执行,立刻打印 1。setTimeout是异步代码,交给**宿主环境(浏览器)**去计时,不会原地等待,所以解析继续往下走,不阻塞后续代码。console.log(3)同步执行,立刻打印 3;此时调用栈空了,事件循环开始反复查看任务队列。- 2 秒后浏览器把回调推入任务队列,调用栈空闲时取出回调执行,打印 2,最终输出
1 3 2。 - 易错点:误以为定时器会“卡住”代码等 2 秒;JS 是单线程,正是靠事件循环才不被耗时代码阻塞。
示例二:宏任务与微任务(先清空微任务队列)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>宏任务与微任务</title>
</head>
<body>
<script>
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
new Promise((resolve) => {
console.log(3)
resolve(4)
}).then((res) => {
console.log(res)
})
new Promise((resolve) => {
console.log(5)
resolve(6)
}).then((res) => {
console.log(res)
})
console.log(7)
</script>
</body>
</html>要点解释与易错点:
new Promise(执行器)中的执行器函数是同步的,所以 3、5 会被立刻打印;resolve只是把状态改为成功,不会立刻执行then回调。.then()里的回调是微任务,进微任务队列排队;setTimeout是宏任务,进宏任务队列排队。- 同步代码跑完打印
1 3 5 7;调用栈空闲后先清空微任务队列(打印 4、6),再执行下一个宏任务(打印 2)。 - 最终输出:
1 3 5 7 4 6 2。 - 易错点:① 以为 Promise 内部也是异步的(执行器是同步的);② 以为
setTimeout(…, 0)会排在then前面;③ 以为微任务只执行一个——实际是全部清空。
示例三:经典面试题(宏任务与微任务穿插)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>事件循环经典面试题</title>
</head>
<body>
<script>
console.log(1)
setTimeout(() => {
console.log(2)
new Promise((resolve) => {
resolve(3)
}).then((res) => {
console.log(res)
})
}, 0)
new Promise((resolve) => {
setTimeout(() => {
console.log(4)
}, 0)
resolve(5)
}).then((res) => {
console.log(res)
})
new Promise((resolve) => {
resolve(6)
}).then((res) => {
console.log(res)
})
console.log(7)
</script>
</body>
</html>要点解释与易错点:
- 最外层
script标签本身就是一个宏任务;解析过程中:console.log(1)同步打印 1。 - 第一个
setTimeout回调进宏任务队列(排第 1);第二个setTimeout写在 Promise 执行器内部,执行器是同步的,所以它也会立刻被交给浏览器,进宏任务队列(排第 2)。 - 两个 Promise 的
resolve已让then回调满足条件,但它们进的是微任务队列,顺序是 5 先、6 后;console.log(7)同步打印。 - 同步阶段输出
1 7;清空微任务队列输出5 6;然后执行第一个宏任务:打印 2,它内部又产生一个微任务,这个微任务要先执行完(打印 3),才轮到下一个宏任务(打印 4)。 - 最终输出:
1 7 5 6 2 3 4。 - 易错点:① 认为先把两个
setTimeout都执行完——实际每执行完一个宏任务都要先清空微任务;② 忘记宏任务内部还能产生新的微任务;③ 这类嵌套代码基本只出现在面试题中,日常开发不会这样写。
常见错误
- 现象:把
setTimeout(0)当成立即执行,分析结果写成同步代码前;原因:把“延迟 0”等同于“同步”;修法:仍按宏任务入队,等同步代码和微任务清空后再执行。 - 现象:认为
new Promise执行器里的代码异步执行;原因:混淆执行器与then/catch回调;修法:执行器同步执行,then/catch回调才是微任务。 - 现象:异步回调一完成就认为立刻执行;原因:忽略调用栈空闲和任务队列;修法:先执行同步代码,调用栈空闲后按队列规则执行。
- 现象:分析顺序时先执行下一个宏任务,再清微任务;原因:忽略微任务优先级;修法:每个宏任务结束后先清空微任务队列,再取下一个宏任务。
- 现象:
Promise.all中任意一个失败后仍以为要等全部完成;原因:把catch触发条件误当成“全部失败”;修法:任一reject立即进入catch,取第一个失败原因。 - 现象:
Promise.all结果数组按响应完成先后排列;原因:误以为按时间排序;修法:结果顺序与传入 Promise 数组顺序一致。 - 现象:
Promise.all数组里放普通值/请求结果,或map回调漏return;原因:没有保证元素是 Promise;修法:确保数组元素为 Promise,map中return请求 Promise。
练习
- 写出打印顺序:
js
console.log(1)
setTimeout(() => console.log(2), 0)
Promise.resolve().then(() => console.log(3))
console.log(4)答案:1 4 3 2
- 写出打印顺序:
js
new Promise((resolve) => {
console.log(1)
resolve()
}).then(() => console.log(2))
console.log(3)答案:1 3 2
- 判断输出:
js
const p1 = Promise.resolve(1)
const p2 = Promise.reject('err')
const p3 = Promise.resolve(3)
Promise.all([p1, p2, p3])
.then(r => console.log('then', r))
.catch(e => console.log('catch', e))答案:catch err
- 写出打印顺序:
js
console.log('A')
setTimeout(() => console.log('B'), 0)
Promise.resolve().then(() => {
console.log('C')
return Promise.resolve()
}).then(() => console.log('D'))
console.log('E')答案:A E C D B
- 三个请求
request1/2/3均返回 Promise,要求全部成功后按request1、request2、request3的顺序输出结果,任一失败输出失败原因。写核心调用。 答案:
js
Promise.all([request1(), request2(), request3()])
.then(([r1, r2, r3]) => console.log(r1, r2, r3))
.catch(err => console.log(err))原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 事件循环-eventloop | 09:47 |
| 2 | 事件循环-练习 | 06:01 |
| 3 | 宏任务和微任务 | 11:53 |
| 4 | 事件循环-经典面试题 | 07:34 |
| 5 | Promise.all静态方法 | 14:20 |
小结
- 同步代码永远先于同轮异步回调执行。
setTimeout(0)不会插队,仍是宏任务。- Promise 执行器同步执行,
then/catch回调是微任务。 - 每轮先清空微任务队列,再取下一个宏任务。
- 用户交互、
load等事件回调需事件触发后才入宏任务队列。 Promise.all成功结果按传入 Promise 数组顺序返回。Promise.all任一失败立即进入catch,取第一个失败原因。- 复杂题按“同步 → 微任务 → 宏任务”逐轮标记分析。