前端老铁别懵圈:搞懂事件循环,从卡顿到丝滑就差这一篇
- 前端老铁别懵圈:搞懂事件循环,从卡顿到丝滑就差这一篇
- 引言:页面卡成 PPT,真不一定是 CSS 的锅
- JavaScript 单线程?别被这三个字吓尿
- 浏览器事件循环:快递分拣中心一日游
- Node.js:表面兄弟,内核各玩各的
- async/await:语法糖里藏着刀片
- 翻车现场一:updated 里疯狂 setState
- 翻车现场二:requestAnimationFrame 里套 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 小车就是可以插队,气不气?
浏览器事件循环:快递分拣中心一日游
把事件循环想象成双十一快递仓:
- 同步代码——老板亲自站在门口,先把手里的件全发完。
- 微任务——顺丰小哥的电动小车,在仓库里来回窜,一次全清。
- 宏任务——圆通大卡车,拉一车货,卸完就走,下一辆再来。
- 渲染——保洁阿姨,只有前面没人排队才进来拖地(画页面)。
来段“看图说话”代码,自己跑一遍比啥都强:
<!doctypehtml><html><body><divid="log"></div><script>constlog=(...args)=>{constp=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');// 微任务里再塞微任务returnPromise.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 属于微任务。
看例子:
asyncfunctionfoo(){console.log('1. 同步进函数');awaitbar();// 把后面一切切成微任务console.log('3. await 后面当成微任务');}asyncfunctionbar(){console.log('2. bar 本身同步');}foo();console.log('4. 函数外同步');// 顺序:1 → 2 → 4 → 3坑点 1:await 后面如果跟的不是 Promise,也会被Promise.resolve()包一层,照样微任务。
坑点 2:多层嵌套,每层都切,一不小心就“微任务风暴”——VIP 小车在仓库里堵成停车场,主线程一样卡。
翻车现场一:updated 里疯狂 setState
Vue 同学看过来,这段熟不熟?
exportdefault{data(){return{count:0};},updated(){// 数据一变我就加,加到地老天荒this.count++;}};updated钩子在 DOM 更新后触发,你里面对数据动手脚,Vue 又得再次更新 DOM,更新完又进updated……
事件循环表示:我拦不住,你自求多福。
解决:要么this.$nextTick里再改,要么用watch加判断,千万别直接 ++。
翻车现场二:requestAnimationFrame 里套 setTimeout
需求:动画帧里隔 16 ms 再干点活,听起来没毛病?
functionloop(){requestAnimationFrame(loop);setTimeout(()=>{// 干点重活,比如算粒子轨迹},0);}帧率直接掉到 10 FPS,为什么?rAF每 16.6 ms 一次,你在里面又塞宏任务,浏览器得等下一帧再干,结果任务越积越多。
正确姿势:把计算放rAF同步里,或者放 Worker,别用setTimeout添堵。
调试骚操作:三行代码写个“迷你事件循环”
// 极简事件循环模拟器,仅供娱乐constmacro=[];constmicro=[];constflushMicro=()=>{while(micro.length)micro.shift()();};construn=()=>{while(macro.length){consttask=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.jsconstworker=newWorker('calc.js');worker.postMessage(bigData);worker.onmessage=e=>console.log('算完了',e.data);
Node:// calc.jsself.onmessage=e=>{constresult=heavyCompute(e.data);self.postMessage(result);};const{Worker}=require('worker_threads');newWorker(`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等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!