鄂州市网站建设_网站建设公司_后端开发_seo优化
2025/12/23 2:14:53 网站建设 项目流程


前端新人必懂: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。它每天的工作就是:

  1. 瞄一眼 JS 调用栈(Stack)空没空;
  2. 空了就跑去任务队列(Queue)里喊:“下一个!”
  3. 把任务抱回栈里执行;
  4. 执行完继续 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.allfor await


Node.js 和浏览器中的事件循环有何不同

浏览器版本上文已经画过,Node 官方给的图更“螺旋”:

┌-------------------------┐ │ poll(I/O 回调) │ └----------┬--------------┘ │check ▼ ┌-------------------------┐ │ check(setImmediate) │ └----------┬--------------┘ │close ▼ ┌-------------------------┐ │ close callbacks │ └----------┬--------------┘ │timers ▼ ┌-------------------------┐ │ timers(setTimeout) │ └-------------------------┘

关键差异:

  1. Node 的宏任务分得更细(timers、poll、check、close 四阶段)。
  2. 微任务在每条宏任务阶段之间都会清一次,而不是“一次宏任务后全部清”。
  3. setImmediatesetTimeout 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

解析速记:

  1. 同步全跑完(D A C F I)
  2. 然后清微任务:B G H
  3. 最后宏任务: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 和断点看清任务队列

  1. 老派 console
    在关键位置打印performance.now()与任务名,肉眼对比时间戳。

  2. 断点 + Call Stack
    Chrome DevTools → Sources → 断点打在thensetTimeout回调里,右侧 Call Stack 会标明 (async),一眼识别是宏任务还是微任务。

  3. Performance 面板
    录制 5 秒,展开 Main 线程,灰色条是宏任务,青色条是微任务,谁长谁短一目了然。

  4. 黑科技 queryObjects
    Console 里执行queryObjects(Promise)可实时查看未被回收的 Promise 数量,内存泄漏排查神器


常见误区:把回调函数等同于异步?别闹了

回调只是异步的通信方式,并不等于异步。
比如:

[1,2,3].forEach(item=>console.log(item));// 同步回调

真正异步的是把回调放到另一条线程或任务队列
所以下次听到“回调地狱”别一脸懵:那是控制流混乱,不是异步原罪


性能优化小贴士:合理安排任务避免卡顿

  1. 长任务拆片
    超过 50ms 的同步任务会掉帧,用setTimeoutrequestIdleCallback拆片:
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();}
  1. 批量 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');
  1. 微任务也别滥用
    微任务虽然优先级高,但太多会阻塞渲染
    做数据 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 单线程慢”,慢的是你把所有活都塞给主线程干


写给未来的自己:一份事件循环速查备忘清单

  1. 同步代码永远最先跑。
  2. 微任务在每次宏任务之后、渲染之前全部清空。
  3. 宏任务按来源排队:定时器 → I/O → 浏览器渲染 → 交互事件
  4. setTimeout 0最少 4ms,别指望“立即”。
  5. async/await是 Promise 的语法糖,await 后面是微任务。
  6. Node 和浏览器宏任务阶段不同,别混用经验
  7. 性能卡顿时:拆片、分帧、读写分离、宏任务兜底。
  8. 调试:console + performance + queryObjects,三板斧。
  9. 锁变量时,判断与置标放同步,别让事件循环插队。
  10. 微任务高优先级,但过量会阻塞渲染,适量即可。

把这份清单贴在工位,下次事件循环再挖坑,就让它循环去吧

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

推荐: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等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询