Skip to content

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);

先别急着运行,按事件循环模型推一遍。

分析前的准备:三个角色和两条队列 ​

为什么需要先摆模型?因为这道题的难点是“宏任务和微任务穿插”,没有模型就只能靠猜。怎么想?记住三个角色:调用栈、宿主环境(浏览器)、任务队列;任务队列又分宏任务队列和微任务队列。怎么做?按下面的规则推:

  1. 同步代码直接进调用栈,执行完出栈;
  2. setTimeout 是宏任务,先交给浏览器,时间到了把回调放进宏任务队列;
  3. new Promise 里的执行器是同步的,会立刻执行;.then 里的回调是微任务,进微任务队列;
  4. 调用栈空闲后,先清空微任务队列,再取下一个宏任务。

易错点: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.all 合并多个 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,并等待所有同时成功的结果”的场景;如果有一个报错,最终会进入失败状态。当需要同时请求多个接口,并把多个接口数据同时渲染到网页上时,就可以使用它。

课件图示 ​

课件第33页:Promise.all 静态方法 说明: 此方法在集合多个promise的返回结

课件第34页:Promise.all 静态方法 语法: 需求: 查询“北京”、“上海”、“广州

课件第27页:定义:每个代理都是由事件循环驱动的,事件循环负责收集用事件(包括用户事件以及其他

课件第29页:总结 总结 1. (宏)任务: 浏览器执行的异步代码: script标签,定时器

代码示例 ​

示例一:事件循环基础(同步代码不等待定时器) ​

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 都执行完——实际每执行完一个宏任务都要先清空微任务;② 忘记宏任务内部还能产生新的微任务;③ 这类嵌套代码基本只出现在面试题中,日常开发不会这样写。

常见错误 ​

  1. 现象:把 setTimeout(0) 当成立即执行,分析结果写成同步代码前;原因:把“延迟 0”等同于“同步”;修法:仍按宏任务入队,等同步代码和微任务清空后再执行。
  2. 现象:认为 new Promise 执行器里的代码异步执行;原因:混淆执行器与 then/catch 回调;修法:执行器同步执行,then/catch 回调才是微任务。
  3. 现象:异步回调一完成就认为立刻执行;原因:忽略调用栈空闲和任务队列;修法:先执行同步代码,调用栈空闲后按队列规则执行。
  4. 现象:分析顺序时先执行下一个宏任务,再清微任务;原因:忽略微任务优先级;修法:每个宏任务结束后先清空微任务队列,再取下一个宏任务。
  5. 现象:Promise.all 中任意一个失败后仍以为要等全部完成;原因:把 catch 触发条件误当成“全部失败”;修法:任一 reject 立即进入 catch,取第一个失败原因。
  6. 现象:Promise.all 结果数组按响应完成先后排列;原因:误以为按时间排序;修法:结果顺序与传入 Promise 数组顺序一致。
  7. 现象:Promise.all 数组里放普通值/请求结果,或 map 回调漏 return;原因:没有保证元素是 Promise;修法:确保数组元素为 Promise,map 中 return 请求 Promise。

练习 ​

  1. 写出打印顺序:
js
console.log(1)
setTimeout(() => console.log(2), 0)
Promise.resolve().then(() => console.log(3))
console.log(4)

答案:1 4 3 2

  1. 写出打印顺序:
js
new Promise((resolve) => {
  console.log(1)
  resolve()
}).then(() => console.log(2))
console.log(3)

答案:1 3 2

  1. 判断输出:
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

  1. 写出打印顺序:
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

  1. 三个请求 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事件循环-eventloop09:47
2事件循环-练习06:01
3宏任务和微任务11:53
4事件循环-经典面试题07:34
5Promise.all静态方法14:20

小结 ​

  1. 同步代码永远先于同轮异步回调执行。
  2. setTimeout(0) 不会插队,仍是宏任务。
  3. Promise 执行器同步执行,then/catch 回调是微任务。
  4. 每轮先清空微任务队列,再取下一个宏任务。
  5. 用户交互、load 等事件回调需事件触发后才入宏任务队列。
  6. Promise.all 成功结果按传入 Promise 数组顺序返回。
  7. Promise.all 任一失败立即进入 catch,取第一个失败原因。
  8. 复杂题按“同步 → 微任务 → 宏任务”逐轮标记分析。