前端老铁别懵圈:搞懂事件循环,从卡顿到丝滑就差这一篇
事件循环就像你和浏览器之间的暗号:你写,浏览器一看——“哦,普通快递,扔宏任务区”;你写,浏览器点头——“VIP 小车,先走!你写await,浏览器嘿嘿一笑——“拆成两段,前面同步,后面微任务,别怪我。听懂它,页面就丝滑;听不懂,它就给你表演“薛定谔的响应”——用户点按钮,到底卡不卡,打开性能面板才知道。别再迷信“16 ms 传说”,也别再复制 StackOverflow 的setTimeout魔

前端老铁别懵圈:搞懂事件循环,从卡顿到丝滑就差这一篇
前端老铁别懵圈:搞懂事件循环,从卡顿到丝滑就差这一篇
友情提示:本文自带大量代码,手机党请自备流量,电脑党请自备咖啡,阅读过程中如有“卧槽原来如此”的惊呼,属正常生理反应,别憋着。
引言:页面卡成 PPT,真不一定是 CSS 的锅
前阵子公司刚来的小兄弟,攥着 16 核 MBP,跑自己写的抽奖动画,一按按钮风扇直接起飞,页面卡得跟 98 年拨号上网似的。
我凑过去一看,代码没死循环、没内存泄漏,就是一堆 setTimeout 套 Promise 再套 async/await,跟俄罗斯套娃似的。
他一脸懵:“哥,我都没写 for(;;),怎么还能卡?”
我叹了口气:“兄弟,你这不是代码卡,是事件循环被你玩成九曲十八弯了。”
于是有了这篇——不拽术语、不画抽象大图,像深夜微信群语音一样,把事件循环这破事儿聊透。
读完你要是还不懂,我把键盘吃了——机械青轴,嚼得嘎嘣脆。
JavaScript 单线程?别被这三个字吓尿
先说结论:JS 确实只有一条主线程,但这条线程背后有一整个“后勤集团”——任务队列、微任务队列、渲染线程、Worker 线程、GPU 线程……
单线程就像只有一个收银台,但后面仓库里有一堆拣货员,还有优先派送给 VIP 的快递小车。
看代码最直观:
// 1. 同步代码,直接塞收银台
console.log('A:我第一个付钱');
// 2. 宏任务,扔进“普通快递”通道
setTimeout(() => console.log('B:普通快递到货'), 0);
// 3. 微任务,走 VIP 小车
Promise.resolve().then(() => console.log('C:VIP 小车闪送'));
// 4. 又一段同步
console.log('D:第二个付钱');
// 打印顺序:A → D → C → B
为什么 setTimeout 明明写 0 毫秒,还比 Promise 慢?
因为“宏任务”一次只取一个,执行完还得把当场所有“微任务”清空才能进行下一轮。
VIP 小车就是可以插队,气不气?
浏览器事件循环:快递分拣中心一日游
把事件循环想象成双十一快递仓:
- 同步代码——老板亲自站在门口,先把手里的件全发完。
- 微任务——顺丰小哥的电动小车,在仓库里来回窜,一次全清。
- 宏任务——圆通大卡车,拉一车货,卸完就走,下一辆再来。
- 渲染——保洁阿姨,只有前面没人排队才进来拖地(画页面)。
来段“看图说话”代码,自己跑一遍比啥都强:
<!doctype html>
<html>
<body>
<div id="log"></div>
<script>
const log = (...args) => {
const p = document.createElement('p');
p.textContent = args.join(' ');
document.getElementById('log').appendChild(p);
};
log('1. 同步代码');
setTimeout(() => log('2. 宏任务① setTimeout'), 0);
Promise.resolve().then(() => {
log('3. 微任务① Promise');
// 微任务里再塞微任务
return Promise.resolve().then(() => log('4. 微任务② 嵌套'));
}).then(() => log('5. 微任务③ 链式'));
queueMicrotask(() => log('6. 微任务④ queueMicrotask'));
requestAnimationFrame(() => log('7. rAF 渲染前'));
log('8. 同步代码尾巴');
</script>
</body>
</html>
跑完顺序:1 → 8 → 3 → 6 → 4 → 5 → 7 → 2
看见没?requestAnimationFrame 卡在渲染前,但仍在宏任务之前,这就是“保洁阿姨”的排班表。
Node.js:表面兄弟,内核各玩各的
浏览器那套在 Node 里基本不认账。Node 基于 libuv,事件循环分六个阶段:
- timers (
setTimeout、setInterval) - pending callbacks (系统错误回调,比如 TCP 错)
- idle, prepare (内部用,你别管)
- poll (等 I/O,大部分代码在这里耗着)
- check (
setImmediate) - close callbacks (关闭文件描述符等)
代码走一个:
// node v18 实测
console.log('A:同步');
setTimeout(() => console.log('B:timer 阶段'), 0);
setImmediate(() => console.log('C:check 阶段'));
Promise.resolve().then(() => console.log('D:微任务'));
// 启动完先跑同步,然后清微任务,再进事件循环
// 打印:A → D → B → C
注意:如果你在 timers 阶段之前先 setImmediate,它可能先执行;但在 REPL 里跑又不一样,libuv 版本不同还会变。
所以 Node 官网才语重心长:别写依赖 setImmediate 和 setTimeout 相对顺序的业务代码,真的会翻车。
async/await:语法糖里藏着刀片
async/await 本质上是 Promise 的语法糖,而 Promise 属于微任务。
看例子:
async function foo() {
console.log('1. 同步进函数');
await bar(); // 把后面一切切成微任务
console.log('3. await 后面当成微任务');
}
async function bar() {
console.log('2. bar 本身同步');
}
foo();
console.log('4. 函数外同步');
// 顺序:1 → 2 → 4 → 3
坑点 1:await 后面如果跟的不是 Promise,也会被 Promise.resolve() 包一层,照样微任务。
坑点 2:多层嵌套,每层都切,一不小心就“微任务风暴”——VIP 小车在仓库里堵成停车场,主线程一样卡。
翻车现场一:updated 里疯狂 setState
Vue 同学看过来,这段熟不熟?
export default {
data() {
return { count: 0 };
},
updated() {
// 数据一变我就加,加到地老天荒
this.count++;
}
};
updated 钩子在 DOM 更新后触发,你里面对数据动手脚,Vue 又得再次更新 DOM,更新完又进 updated……
事件循环表示:我拦不住,你自求多福。
解决:要么 this.$nextTick 里再改,要么用 watch 加判断,千万别直接 ++。
翻车现场二:requestAnimationFrame 里套 setTimeout
需求:动画帧里隔 16 ms 再干点活,听起来没毛病?
function loop() {
requestAnimationFrame(loop);
setTimeout(() => {
// 干点重活,比如算粒子轨迹
}, 0);
}
帧率直接掉到 10 FPS,为什么?rAF 每 16.6 ms 一次,你在里面又塞宏任务,浏览器得等下一帧再干,结果任务越积越多。
正确姿势:把计算放 rAF 同步里,或者放 Worker,别用 setTimeout 添堵。
调试骚操作:三行代码写个“迷你事件循环”
// 极简事件循环模拟器,仅供娱乐
const macro = [];
const micro = [];
const flushMicro = () => { while (micro.length) micro.shift()(); };
const run = () => {
while (macro.length) {
const task = macro.shift();
task();
flushMicro(); // 每执行一个宏任务就清微任务
}
};
// 塞任务
macro.push(() => console.log('宏①'));
macro.push(() => console.log('宏②'));
micro.push(() => console.log('微①'));
run(); // 打印:宏① → 微① → 宏②
浏览器真事件循环比这复杂 114514 倍,但道理一样:先宏,后微,再渲染,循环往复。
自己动动手,印象比看八篇知乎都深。
性能面板:眼见为实
Chrome DevTools → Performance → 录一段卡顿操作 → 看 Task 切片
紫色长条是渲染,灰色是脚本,黄色是 GC,红色是 FPS 掉底。
如果灰色条里套娃一样出现大量 Promise.then,恭喜你,微任务风暴实锤。
优化思路:
- 把大计算拆段,用
setTimeout或queueMicrotask做“时间切片”; - 能上 Worker 就上 Worker,别让主线程当苦力;
- 避免在微任务里继续塞微任务,链式 Promise 能合并就合并。
小技巧汇总:让代码丝滑到老板怀疑你开了外挂
- 优先微任务?别走火入魔
微任务执行时机早,但太多会阻塞渲染,用户照样卡。 - Node 环境牢记
nextTick>Promise>setImmediate>setTimeout
官方文档自己都吐槽:process.nextTick是“插队狂魔”,慎用。 - 浏览器里想“比微任务还早”?用
MutationObserver
监听 DOM 变动,回调也是微任务,但比 Promise 还早一丢丢,黑科技专用。 - 动画里别写同步死循环
while (Date.now() - start < 16) {}这种蠢事,我年轻时真干过,风扇起飞算轻的,电脑差点原地升天。 - 真·计算密集就上 Worker
浏览器:// main.js const worker = new Worker('calc.js'); worker.postMessage(bigData); worker.onmessage = e => console.log('算完了', e.data);
Node:// calc.js self.onmessage = e => { const result = heavyCompute(e.data); self.postMessage(result); };const { Worker } = require('worker_threads'); new Worker(` const { parentPort } = require('worker_threads'); parentPort.on('message', data => { parentPort.postMessage(heavy(data)); }); `, { eval: true });
结语:听懂事件循环,代码才把你当自己人
事件循环就像你和浏览器之间的暗号:
你写 setTimeout(cb, 0),浏览器一看——“哦,普通快递,扔宏任务区”;
你写 Promise.then,浏览器点头——“VIP 小车,先走!”;
你写 await,浏览器嘿嘿一笑——“拆成两段,前面同步,后面微任务,别怪我。”
听懂它,页面就丝滑;听不懂,它就给你表演“薛定谔的响应”——用户点按钮,到底卡不卡,打开性能面板才知道。
别再迷信“16 ms 传说”,也别再复制 StackOverflow 的 setTimeout 魔法数字。
记住:单线程不可怕,可怕的是你对它的后勤集团一无所知。
今晚回去把项目翻一遍,看到嵌套 setTimeout 就改,看到 updated 里 ++ 就删,看到大量 await 就画调用图。
改完再测 FPS,如果帧率稳成直线,记得回来请我吃烤串——我要机械青轴味的。
欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!
| 专栏系列(点击解锁) | 学习路线(点击解锁) | 知识定位 |
|---|---|---|
| 《微信小程序相关博客》 | 持续更新中~ | 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等 |
| 《AIGC相关博客》 | 持续更新中~ | AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结 |
| 《HTML网站开发相关》 | 《前端基础入门三大核心之html相关博客》 | 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识 |
| 《前端基础入门三大核心之JS相关博客》 | 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。 通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心 |
|
| 《前端基础入门三大核心之CSS相关博客》 | 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页 | |
| 《canvas绘图相关博客》 | Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化 | |
| 《Vue实战相关博客》 | 持续更新中~ | 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅 |
| 《python相关博客》 | 持续更新中~ | Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具 |
| 《sql数据库相关博客》 | 持续更新中~ | SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能 |
| 《算法系列相关博客》 | 持续更新中~ | 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维 |
| 《IT信息技术相关博客》 | 持续更新中~ | 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识 |
| 《信息化人员基础技能知识相关博客》 | 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方 | |
| 《信息化技能面试宝典相关博客》 | 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面 | |
| 《前端开发习惯与小技巧相关博客》 | 持续更新中~ | 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等 |
| 《photoshop相关博客》 | 持续更新中~ | 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结 |
| 日常开发&办公&生产【实用工具】分享相关博客》 | 持续更新中~ | 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

更多推荐


所有评论(0)