同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
一、开篇:一个让你怀疑人生的 console.log
先看这段代码:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
实际输出是:1 4 3 2,而不是 1 2 3 4。
很多人会懵:setTimeout 延迟是 0,按理说应该“马上执行”,为什么反而比 Promise 还晚?这就涉及到 JavaScript 的事件循环和宏任务、微任务。
二、概念扫盲
先把几个核心概念搞清楚:
| 概念 | 一句话 |
|---|---|
| 调用栈 | 同步代码执行的地方,先进后出 |
| 事件循环 | 不断检查「调用栈空了没 → 执行微任务 → 执行一个宏任务」的循环 |
| 宏任务 | script、setTimeout、setInterval、I/O 等 |
| 微任务 | Promise.then/catch/finally、queueMicrotask、MutationObserver 等 |
核心要点:每执行完一个宏任务,会先清空当前所有微任务,再执行下一个宏任务。
大白话:可以把 JS 想象成一个办事员——先把手头的事(同步代码)干完,再去处理“小纸条”(微任务),最后才去处理“排队的人”(下一个宏任务)。微任务永远插队在下一个宏任务前面。
三、核心规则(3 条记住就够用)
- 同步代码先执行完;
- 同步代码执行完后,先执行当前所有微任务;
- 微任务清空后,再执行下一个宏任务,然后重复 2~3。
记住:同步 > 微任务 > 下一个宏任务。
四、典型例子
1:setTimeout vs Promise(最基础)
console.log('1'); // 同步
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve()
.then(() => console.log('3')) // 微任务
.then(() => console.log('5')); // 微任务
console.log('4'); // 同步
输出:1 4 3 5 2
分步讲解:
| 阶段 | 发生了什么 | 输出 |
|---|---|---|
| 主脚本执行 | 执行 1、4;把 setTimeout 回调放进宏任务队列;把 Promise.then 回调放进微任务队列 | 1, 4 |
| 微任务阶段 | 主脚本(第一个宏任务)执行完,清空微任务队列 | 3, 5 |
| 下一个宏任务 | 执行 setTimeout 回调 | 2 |
同步先跑完,微任务插队在下一个宏任务前面,所以 2 一定在最后。
2:嵌套场景(Promise 里套 setTimeout)
console.log('start');
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => console.log('promise1'));
}, 0);
Promise.resolve()
.then(() => {
console.log('promise2');
setTimeout(() => console.log('timer2'), 0);
});
console.log('end');
输出:start end promise2 timer1 promise1 timer2
分步讲解:
| 阶段 | 发生了什么 | 输出 |
|---|---|---|
| 主脚本 | 同步执行 start、end;注册 timer1、promise2 | start, end |
| 微任务 | 执行 promise2,其中又注册了 timer2 | promise2 |
| 宏任务 timer1 | 执行 timer1,又注册了 promise1 | timer1 |
| 微任务 | timer1 执行完后,清空微任务,执行 promise1 | promise1 |
| 宏任务 timer2 | 执行 timer2 | timer2 |
要点:宏任务里新产生的微任务,会在当前宏任务结束后立即执行,不会等到下一轮。所以 timer1 跑完 → 立刻执行 promise1 → 再执行 timer2。很多面试题的“怪顺序”都是这种嵌套造成的。
3:async/await 的本质
async function foo() {
console.log('foo start');
await bar();
console.log('foo end'); // 等价于 Promise.then 里的代码
}
async function bar() {
console.log('bar');
}
console.log('script start');
foo();
console.log('script end');
输出:script start → foo start → bar → script end → foo end
等价理解:await 之后的代码会被包进微任务:
// await bar() 等价于:
// Promise.resolve(bar()).then(() => { console.log('foo end'); })
为什么 foo end 在 script end 之后? 因为 await bar() 会把后面的 console.log('foo end') 包装进 Promise.then 的回调,本质是微任务。微任务要等当前宏任务(主脚本)执行完才跑,所以 script end 先输出,foo end 后输出。
五、日常怎么选、踩坑在哪
1.选型建议
| 场景 | 建议 | 原因 |
|---|---|---|
| 希望“尽快”执行 | 用 Promise.then 或 queueMicrotask | 微任务在下一宏任务之前执行 |
| 需要推迟到“下一轮” | 用 setTimeout(fn, 0) | 作为宏任务,排在当前微任务之后 |
| 不想阻塞 UI | 长计算用 requestIdleCallback 或分片 | setTimeout 0 仍可能卡顿 |
为什么“尽快执行”不推荐 setTimeout(fn, 0)? 即使延迟为 0,回调也会进入宏任务队列,必须等当前所有微任务都执行完才会轮到自己。如果中间有很多微任务,你的回调会被推迟。
2.常见踩坑
- 以为 setTimeout(fn, 0) 是“马上执行”:其实是“当前宏任务 + 微任务都跑完后”才执行;
- 以为 await 是同步:
await之后的代码是微任务; - 微任务里再注册微任务:会在同一轮微任务阶段继续执行,可能造成长微任务链,阻塞渲染;
- 在微任务里做耗时计算:微任务会一口气执行完,中间不会让出主线程,可能卡住页面。
六、小结
| 例子 | 核心考点 | 一句话记住 |
|---|---|---|
| 例子 1 | 同步 > 微任务 > 宏任务 | 1 4 先,3 5 再,2 最后 |
| 例子 2 | 宏任务里产生的微任务,当前宏任务结束后马上清空 | timer1 跑完 → 立刻执行 promise1 → 再执行 timer2 |
| 例子 3 | await 后面的代码是微任务 | foo end 不会在 script end 之前 |
记住四句话:
- 同步 > 微任务 > 下一个宏任务;
Promise.then、async/await的回调是微任务;setTimeout、setInterval是宏任务;- 需要“尽快但不阻塞”时用微任务;需要“让出本轮”时用
setTimeout(fn, 0)。
延伸阅读:HTML 规范 Event loops、Node.js 中 process.nextTick vs setImmediate vs setTimeout(与浏览器有差异)。
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~