在这里插入图片描述

前端老铁别懵圈:搞懂事件循环,从卡顿到丝滑就差这一篇

友情提示:本文自带大量代码,手机党请自备流量,电脑党请自备咖啡,阅读过程中如有“卧槽原来如此”的惊呼,属正常生理反应,别憋着。


引言:页面卡成 PPT,真不一定是 CSS 的锅

前阵子公司刚来的小兄弟,攥着 16 核 MBP,跑自己写的抽奖动画,一按按钮风扇直接起飞,页面卡得跟 98 年拨号上网似的。
我凑过去一看,代码没死循环、没内存泄漏,就是一堆 setTimeoutPromise 再套 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 小车就是可以插队,气不气?


浏览器事件循环:快递分拣中心一日游

把事件循环想象成双十一快递仓:

  1. 同步代码——老板亲自站在门口,先把手里的件全发完。
  2. 微任务——顺丰小哥的电动小车,在仓库里来回窜,一次全清。
  3. 宏任务——圆通大卡车,拉一车货,卸完就走,下一辆再来。
  4. 渲染——保洁阿姨,只有前面没人排队才进来拖地(画页面)。

来段“看图说话”代码,自己跑一遍比啥都强:

<!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,事件循环分六个阶段:

  1. timers (setTimeoutsetInterval
  2. pending callbacks (系统错误回调,比如 TCP 错)
  3. idle, prepare (内部用,你别管)
  4. poll (等 I/O,大部分代码在这里耗着)
  5. check (setImmediate
  6. 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 官网才语重心长:别写依赖 setImmediatesetTimeout 相对顺序的业务代码,真的会翻车。


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,恭喜你,微任务风暴实锤。
优化思路:

  1. 把大计算拆段,用 setTimeoutqueueMicrotask 做“时间切片”;
  2. 能上 Worker 就上 Worker,别让主线程当苦力;
  3. 避免在微任务里继续塞微任务,链式 Promise 能合并就合并。

小技巧汇总:让代码丝滑到老板怀疑你开了外挂

  1. 优先微任务?别走火入魔
    微任务执行时机早,但太多会阻塞渲染,用户照样卡。
  2. Node 环境牢记 nextTick > Promise > setImmediate > setTimeout
    官方文档自己都吐槽:process.nextTick 是“插队狂魔”,慎用。
  3. 浏览器里想“比微任务还早”?用 MutationObserver
    监听 DOM 变动,回调也是微任务,但比 Promise 还早一丢丢,黑科技专用。
  4. 动画里别写同步死循环
    while (Date.now() - start < 16) {} 这种蠢事,我年轻时真干过,风扇起飞算轻的,电脑差点原地升天。
  5. 真·计算密集就上 Worker
    浏览器:
    // main.js
    const worker = new Worker('calc.js');
    worker.postMessage(bigData);
    worker.onmessage = e => console.log('算完了', e.data);
    
    // calc.js
    self.onmessage = e => {
      const result = heavyCompute(e.data);
      self.postMessage(result);
    };
    
    Node:
    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等工具

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

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

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐