前端新人必懂:JavaScript事件循环机制全解析(附实战避坑指南)
- 前端新人必懂:JavaScript事件循环机制全解析(附实战避坑指南)
- 引言:你写的代码真的按顺序执行了吗?
- 揭开浏览器背后神秘的调度员——事件循环
- 同步与异步代码如何共处一室
- 宏任务与微任务:谁先谁后不是随便定的
- setTimeout(fn, 0) 真的是“立刻”执行吗?
- Promise、async/await 背后的微任务秘密
- Node.js 和浏览器中的事件循环有何不同
- 面试高频题:事件循环执行顺序大揭秘
- 真实项目中因事件循环引发的诡异Bug复盘
- 调试技巧:如何用 console 和断点看清任务队列
- 常见误区:把回调函数等同于异步?别闹了
- 性能优化小贴士:合理安排任务避免卡顿
- 进阶玩法:利用 queueMicrotask 手动插入微任务
- 当你遇到“明明加了 await 却还是乱序”的时候
- 别再被“JS是单线程”这句话骗了,它其实很忙
- 写给未来的自己:一份事件循环速查备忘清单
前端新人必懂:JavaScript事件循环机制全解析(附实战避坑指南)
警告:下文可能让你对
setTimeout(fn, 0)产生永久性心理阴影,阅读前请自备瓜子、可乐与 debugger。
引言:你写的代码真的按顺序执行了吗?
先来个热身题,把下面这段代码丢进控制台,猜输出:
console.log('A');setTimeout(()=>console.log('B'),0);Promise.resolve().then(()=>console.log('C'));console.log('D');新手版答案:A → B → C → D
进阶版答案:A → D → C → B
灵魂版答案:浏览器说“你猜对了算我输”。
如果你第一次就答对,恭喜,你已经拿到了“事件循环免跪金牌”;答错也别灰心,接下来咱们把这段代码拆成原子级片段,再一点点拼回成能跑的大脑。
揭开浏览器背后神秘的调度员——事件循环
浏览器里藏着一位“调度员”,工牌上写着Event Loop。它每天的工作就是:
- 瞄一眼 JS 调用栈(Stack)空没空;
- 空了就跑去任务队列(Queue)里喊:“下一个!”
- 把任务抱回栈里执行;
- 执行完继续 1.
听起来像地铁安检员,但别小看它——只要它罢工,整个 Tab 直接卡成 PPT。
画张草图(假装有图):
┌---------------------------┐ │ JS Stack(单线程) │ │ 正在执行的函数帧们 │ └------------┬--------------┘ │ pop & push ▼ ┌---------------------------┐ │ Event Loop 安检口 │ └------------┬--------------┘ │ ┌------┴-------┐ ▼ ▼ ┌-----------┐ ┌-----------┐ │ 宏任务队列 │ │ 微任务队列 │ │ setTimeout │ │ Promise.then │ │ Message │ │ queueMicrotask│ └-----------┐ └-----------┘规则只有一句:“一次宏任务 → 全部微任务 → 渲染 → 下一次宏任务”。背不下来就抄手心,面试用得上。
同步与异步代码如何共处一室
先厘清“同步”和“异步”这两个老冤家:
- 同步:排着队,一个嗝打不出来下一个别想动。
- 异步:先领号,去旁边喝咖啡,轮到你了广播喊号。
JS 是单线程,却又要假装自己“不卡”,于是把异步活儿外包给浏览器别的线程(网络、定时器、DOM 事件等),干完了把回调塞进队列,主线程闲了再拎回来执行。
代码举例:
// 同步部分functionsync(){console.log('我是同步,我先跑');}// 异步部分functionasyncPart(){fetch('/api/user')// 浏览器别的线程去下载.then(res=>res.json())// 下载完把回调塞进微任务.then(data=>console.log('网络回来啦',data));}sync();asyncPart();console.log('主线直接跑到这');输出顺序:
我是同步,我先跑 主线直接跑到这 ...(网络回来后才打印)网络回来啦 {...}宏任务与微任务:谁先谁后不是随便定的
事件循环里有两条队伍:
| 队伍 | 常见成员 | 优先级 |
|---|---|---|
| 宏任务 | setTimeout、setInterval、I/O、Message | 低 |
| 微任务 | Promise.then、async/await、queueMicrotask | 高 |
一次循环只拿一个宏任务,但会把当前循环产生的所有微任务一口气全清掉。
换句人话:微任务是“加塞王者”,宏任务只能乖乖排号。
再来段代码感受下“加塞”:
console.log('1. 同步');setTimeout(()=>{// 宏任务1console.log('2. 宏任务');Promise.resolve().then(()=>console.log('3. 宏任务里的微任务'));},0);Promise.resolve().then(()=>{// 微任务1console.log('4. 微任务');setTimeout(()=>console.log('5. 微任务里的宏任务'),0);});console.log('6. 同步');结果:
1. 同步 6. 同步 4. 微任务 2. 宏任务 3. 宏任务里的微任务 5. 微任务里的宏任务背口诀:“同 → 微 → 宏”,面试写白板时先写这六个字,再画图,HR 会多看你两眼。
setTimeout(fn, 0) 真的是“立刻”执行吗?
setTimeout(cb, 0)是前端界最著名的“白色谎言”。
它真正的意思是:“浏览器,你闲了再帮我把 cb 插进宏任务,但最低也要 4ms”(HTML5 标准在嵌套层级深时强制 4ms)。
实验代码:
conststart=performance.now();setTimeout(()=>{console.log('我迟到了',performance.now()-start,'ms');},0);跑出来的数字大概率 ≥ 4ms。
所以下次再看到“setTimeout 0 立即执行”的注释,直接上去 PR:把“立即”改成“尽快”。
Promise、async/await 背后的微任务秘密
Promise 的then/catch/finally和 async/await 本质上都是微任务。
它们比宏任务快,但快不过同步代码。
async/await 只是 Promise 的语法糖,写起来像同步,跑起来还是微任务:
asyncfunctionfoo(){console.log('async 函数里的同步部分');await1;// 把后面的代码包进 Promise.thenconsole.log('await 后面的代码是微任务');}foo();console.log('主线程继续同步');// 输出:// async 函数里的同步部分// 主线程继续同步// await 后面的代码是微任务坑点提示:
如果你在 for 循环里await,会一次一次地把微任务插进队列,量大时可能造成别的宏任务饥饿;做批处理时记得用Promise.all或for await。
Node.js 和浏览器中的事件循环有何不同
浏览器版本上文已经画过,Node 官方给的图更“螺旋”:
┌-------------------------┐ │ poll(I/O 回调) │ └----------┬--------------┘ │check ▼ ┌-------------------------┐ │ check(setImmediate) │ └----------┬--------------┘ │close ▼ ┌-------------------------┐ │ close callbacks │ └----------┬--------------┘ │timers ▼ ┌-------------------------┐ │ timers(setTimeout) │ └-------------------------┘关键差异:
- Node 的宏任务分得更细(timers、poll、check、close 四阶段)。
- 微任务在每条宏任务阶段之间都会清一次,而不是“一次宏任务后全部清”。
setImmediate与setTimeout 0在 Node 里顺序不确定,取决于当前处于哪个阶段;面试常考。
验证代码(Node 环境):
setTimeout(()=>console.log('timer'),0);setImmediate(()=>console.log('immediate'));// 输出可能是 timer → immediate,也可能相反面试高频题:事件循环执行顺序大揭秘
把下面这坨“面试题中的战斗机”粘进浏览器,预测结果:
asyncfunctionasync1(){console.log('A');awaitasync2();console.log('B');}asyncfunctionasync2(){console.log('C');}console.log('D');setTimeout(()=>console.log('E'),0);async1();newPromise((resolve)=>{console.log('F');resolve();}).then(()=>{console.log('G');}).then(()=>{console.log('H');});console.log('I');标准答案:
D → A → C → F → I → B → G → H → E解析速记:
- 同步全跑完(D A C F I)
- 然后清微任务:B G H
- 最后宏任务:E
背不下来?抄手心 +1。
真实项目中因事件循环引发的诡异Bug复盘
案例:支付按钮连点三次,订单却生成了四条记录。
伪代码:
letcreating=false;asyncfunctioncreateOrder(){if(creating)return;creating=true;awaitapi.create();// 异步creating=false;}现象:快速点击三次,后端却收到 4 条订单。
原因:await api.create()期间把控制权还给事件循环,微任务队列里后续的 createOrder 又跑了一次。
修复:用“锁”扩写,把判断和置标放到同一步微任务:
letlocked=false;functioncreateOrder(){if(locked)return;locked=true;api.create().then(()=>{locked=false;}).catch(()=>{locked=false;});}或者直接用同步锁(如果框架允许):
functioncreateOrder(){returnnavigator.locks.request('order',async()=>{awaitapi.create();});}调试技巧:如何用 console 和断点看清任务队列
老派 console
在关键位置打印performance.now()与任务名,肉眼对比时间戳。断点 + Call Stack
Chrome DevTools → Sources → 断点打在then或setTimeout回调里,右侧 Call Stack 会标明 (async),一眼识别是宏任务还是微任务。Performance 面板
录制 5 秒,展开 Main 线程,灰色条是宏任务,青色条是微任务,谁长谁短一目了然。黑科技 queryObjects
Console 里执行queryObjects(Promise)可实时查看未被回收的 Promise 数量,内存泄漏排查神器。
常见误区:把回调函数等同于异步?别闹了
回调只是异步的通信方式,并不等于异步。
比如:
[1,2,3].forEach(item=>console.log(item));// 同步回调真正异步的是把回调放到另一条线程或任务队列。
所以下次听到“回调地狱”别一脸懵:那是控制流混乱,不是异步原罪。
性能优化小贴士:合理安排任务避免卡顿
- 长任务拆片
超过 50ms 的同步任务会掉帧,用setTimeout或requestIdleCallback拆片:
functionbigCalc(list,cb){leti=0;function_run(){conststart=performance.now();for(;i<list.length&&performance.now()-start<16;i++){/* 处理 list[i] */}if(i<list.length){setTimeout(_run);// 扔到宏任务,让浏览器喘口气}else{cb();}}_run();}- 批量 DOM 读写分离
读会强制同步布局,写会触发重排,先读后写能省一次重排:
// 坏:写→读→写→读for(consteloflist){el.style.width='100px';console.log(el.clientHeight);// 强制同步布局}// 好:读→写constheights=list.map(el=>el.clientHeight);list.forEach((el,i)=>el.style.width='100px');- 微任务也别滥用
微任务虽然优先级高,但太多会阻塞渲染。
做数据 diff 时,如果量级上万,分帧 + 宏任务更靠谱。
进阶玩法:利用 queueMicrotask 手动插入微任务
官方提供的“后门”:queueMicrotask(fn)。
它比Promise.resolve().then(fn)更纯粹,不创建 Promise 对象,内存开销更小。
场景:写库时需要在当前宏任务末尾做点事,但又不想污染调用者的 Promise 链:
functionscheduleWork(){queueMicrotask(()=>{console.log('我在微任务里偷偷执行,调用者无感知');});}注意:别在微任务里再狂塞微任务,会饿死宏任务和渲染。
当你遇到“明明加了 await 却还是乱序”的时候
大概率是你await 错了对象。
functiondelay(){setTimeout(()=>{},0);// 忘了 return Promise}asyncfunctionmain(){console.log('1');awaitdelay();// 实际上同步完成console.log('2');}main();输出:1 → 2,中间没有延迟。
正确姿势:
constdelay=ms=>newPromise(r=>setTimeout(r,ms));asyncfunctionmain(){console.log('1');awaitdelay(0);console.log('2');}别再被“JS是单线程”这句话骗了,它其实很忙
单线程指的是执行 JS 代码的线程只有一个,但浏览器背后:
- 网络线程在下载资源
- 合成线程在画图层
- 光栅线程在刷像素
- 定时器线程在数秒
事件循环就是项目经理,把外包团队的结果不断集成回主线程。
所以别再吐槽“JS 单线程慢”,慢的是你把所有活都塞给主线程干。
写给未来的自己:一份事件循环速查备忘清单
- 同步代码永远最先跑。
- 微任务在每次宏任务之后、渲染之前全部清空。
- 宏任务按来源排队:定时器 → I/O → 浏览器渲染 → 交互事件。
setTimeout 0最少 4ms,别指望“立即”。async/await是 Promise 的语法糖,await 后面是微任务。- Node 和浏览器宏任务阶段不同,别混用经验。
- 性能卡顿时:拆片、分帧、读写分离、宏任务兜底。
- 调试:console + performance + queryObjects,三板斧。
- 锁变量时,判断与置标放同步,别让事件循环插队。
- 微任务高优先级,但过量会阻塞渲染,适量即可。
把这份清单贴在工位,下次事件循环再挖坑,就让它循环去吧!
欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐: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等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!