4.1这节课,讲的是事件循环。坦白说,排课的时候我犹豫过,要不要把事件循环放到更靠后的章节,因为它牵扯的东西太多,既有浏览器渲染机制,又有任务队列的调度优先级,初学者一个不留神就会被绕进去。但后来我改了主意:与其让学生先写一堆“能跑但不知道为什么能跑”的异步代码,不如从这天开始,把一个最简单的定时器掰开揉碎,把事件循环的骨架立起来。这是我带项目时最深的体会——懂事件循环的人,写异步代码是心里有地图的;不懂的人,只是在一遍遍试错。
这节“4.1上课内容”,本质上是围绕 JavaScript 异步机制的一节底层原理课。适合刚接触异步编程的前端新人,也适合写了两年代码但遇到性能问题就发怵的业务开发。我会把课堂上的板书逻辑、示例代码、课堂练习和踩坑实录完整梳理一遍,让你即使不在教室里,也能按同样的顺序把知识点捡起来。
1. 这节课要解决的第一个问题:为什么定时器不准时——从setTimeout说起
1.1 一个看起来“不守时”的定时器
上课第一件事,我没有直接讲事件循环的定义,而是先在黑板上写了三行代码:
javascript复制console.log('A');
setTimeout(() => {
console.log('B');
}, 0);
console.log('C');
我让全班同学猜输出顺序。大部分人都答对了,是 A、C、B。但当我追问一句“setTimeout 的延迟时间是 0,为什么 B 还是排在 C 后面”时,教室里安静了几秒钟。这个安静特别有价值,因为它暴露了一个关键问题:大家背过八股文,知道“定时器要等主线程执行完才执行”,却说不清背后的调度机制到底长什么样。
实际上,setTimeout 并不是“延迟执行”,而是“延迟加入任务队列”。浏览器在调用 setTimeout(fn, 0) 时,会启动一个计时器,计时结束后把回调函数塞进宏任务队列,而不是立刻放到调用栈上。主线程当前正在执行的脚本(也就是那两行 console.log)必须先把调用栈清空,事件循环才会回头看一眼宏任务队列。
所以,“定时器不准时”是它的设计使然,不是 bug。它天生就是一个“最短延迟时间”,而不是“精确执行时间”。如果前面有一个耗时的同步任务卡了 200 毫秒,哪怕定时器设置的延迟是 0,回调也得等那 200 毫秒耗完才有机会执行。
1.2 事件循环在这里扮演的角色
接着我会在白板上画一个简化模型:调用栈(Call Stack)在左边,任务队列在右边,中间有一个不断转圈的小人,就是事件循环。主线程上的同步代码进调用栈,执行完就出栈;异步代码则由对应的宿主环境(浏览器或 Node.js)接管,等时机成熟再排进任务队列;事件循环的职责,就是不断检查“调用栈是否为空”,为空就取一个任务出来执行。
我特别强调一点:事件循环本质上是个循环,它不创造任务,只负责搬运。每转一圈,会经历“执行宏任务 -> 清空微任务 -> 必要时渲染”这样一个节奏。同学们一开始很容易把事件循环理解成“一个从上往下执行的队列”,这个误解必须纠正。任务队列不是一个,而是至少两个:宏任务队列和微任务队列。它们的优先级差异,才是这节 4.1 课程内容里最容易考倒人的地方。
1.3 课程设计逻辑:为什么先讲概念而不是API
我之前听过不少速成课,一上来就教 Promise.all、async/await 的用法,学生跟着照抄能跑通,但换个场景就蒙了。所以这节课我刻意反过来,先让大家理解事件循环作为底层引擎的运作方式,再回到 API 层面看用法。你在写 await fetch(...) 的时候,本质上是把后续代码包成了一个微任务;你在写 setTimeout 做轮询的时候,本质上是向宏任务队列投递任务。明白了这层映射关系,很多“魔法”就消失了。
课堂上有同学问我:老师,是不是把事件循环背熟就能拿高薪?我说不是,能把事件循环讲清楚的人,值钱的地方在于他能解释“为什么这段代码会把页面卡死”“为什么这个接口要等 3 秒才返回”。所以从 4.1 开始,每讲一个机制,我都会配套一个实际现象,让机制和现象互相印证。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 宏任务与微任务:课堂板书里的那张执行顺序表
2.1 用食堂打饭的类比理解两类任务
宏任务和微任务这个概念,光靠定义硬记很容易混。我在课上用了一个食堂打饭的类比:宏任务像是排队打饭的同学,每个人打到饭之后,会从叫号屏上取下自己的餐;微任务则是你端着餐盘刚坐下,突然想起“我还要加个咸菜”——只要你愿意,你可以在一个同学打
