资讯动态

前端精读:深入 JavaScript 事件循环(Event Loop)与异步编程原理

发布时间:2026/10/1 17:02:19 来源:尧图企业网站定制
文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载事件循环Event Loop是 JavaScript 运行机制的核心内科它决定了setTimeout、Promise、fetch这些异步代码究竟在什么时机、以什么顺序被执行。本文以本期周刊精读的《Javascript 事件循环与异步》为骨架结合仓库内 Tasks, microtasks 精读、setState 原理精读 等系列文章帮你彻底打通 Call Stack、宿主环境、Microtask/Macrotask 之间的关系并给出一个 React 生命周期中可复现的异步setState实验读完即可在实际项目中写出更可靠、可预期的异步代码。1 引言为什么事件循环像一门内科本期精读的原始文章出自 sessionstack 的 How JavaScript works 系列该系列深入探讨 JS 内部原理并指出只有深入了解 JS 的工作方式才可能写出更好的代码。事件循环之于 JavaScript 开发者就像内科之于医生我们平时只关注外科创伤表层 API 用法却忽视内科问题——setTimeout、Promise这些基础概念如果只停留在浮层理解代码往往会在意想不到的地方出问题。对前端新人来说事件循环也几乎是面试的必考基础题。本文不展开原文的 Promise、async/await 部分聚焦 Event Loop 本身它如何与 Call Stack、宿主环境协作以及 Microtask 与 Macrotask 两种异步队列的差异。2 Event Loop 与 Call Stack、Web APIs 之间的关系2.1 三个角色各司其职在浏览器与 Node.js 中一次 JS 代码的执行涉及三个关键角色Call Stack调用栈所有同步代码的执行场所遵循后进先出LIFO规则。函数调用时入栈函数返回时出栈。Event Loop事件循环本篇文章的主角负责监控调用栈与异步队列在合适时机把异步回调推回调用栈执行。Web APIs宿主环境泛指浏览器提供的 APIDOM、fetch、定时器等或 Node.js 中的底层 C 能力。异步时机的判定由宿主环境负责而不由 JS 引擎负责。原文档用 16 张图演示了 5 行代码的完整执行过程核心结论可以概括为一句话任何同步代码只存在于 Call Stack 中只有异步代码不一定是回调才会进入 Event Loop 的队列。2.2 哪些代码属于异步典型的异步代码包括setTimeout() setInterval() Promise.resolve().then() fetch().then()这些异步代码在执行时不会直接进入 Call Stack而是进入 Event Loop 队列。当 JS 主线程Call Stack执行完毕、且异步时机已到Event Loop 才会把异步回调中的代码推入 Call Stack 执行。2.3 执行时机由宿主环境决定一个容易忽略的关键点异步代码何时开始执行是由宿主环境决定的而不是 JS 引擎。原因是当调用栈清空后JS 引擎本身不会再主动执行任何代码除非 Event Loop 队列中有内容被推送回 Call Stack。以fetch为例JS 调用浏览器发送请求后直到浏览器主动通知 JS请求已完成之前JS 无法干预任何环节——网络 IO 的调度完全在宿主环境浏览器/Node.js内部完成。这也解释了为什么setTimeout(fn, 0)并不是立即执行它只是把回调交给宿主环境的定时器线程计时倒计时结束后再把回调送入事件循环队列等待调用栈空闲。3 Microtask 与 Macrotask两种异步队列3.1 一维队列是不够的事件循环处理异步的方式分两种Macrotask宏任务setTimeout、setInterval之流。Microtask微任务Promise之流。原文档给出了一个非常直观的模型异步队列是周而复始循环执行的可以看作一个二维数组——横排是当前队列中的每一个函数纵排是每一个队列。Macrotask的方式是将执行函数添加到新的纵排新开一个队列Microtask将执行函数添加到当前执行到队列的横排插入到当前队列尾部。因此Microtask的插入是轻量的能最快被执行到——它不需要等待整条宏任务队列轮转只要当前宏任务结束后即可立刻消费。3.2 用真实输出验证执行顺序仓库内 Tasks, microtasks, queues and schedules 精读 给出了一个经典验证用例console.log(script start); setTimeout(function () { console.log(setTimeout); }, 0); Promise.resolve() .then(function () { console.log(promise1); }) .then(function () { console.log(promise2); }); console.log(script end);正确输出顺序是script start script end promise1 promise2 setTimeout原因如下同步脚本执行优先级最高script start、script end先后打印Promise的回调进入MicrotaskssetTimeout的回调进入Tasks一次事件循环中Microtasks 优先于 Tasks 执行Microtasks 执行期间新插入的 Microtaskpromise2会继续按顺序消费直到队列清空才会轮到 Tasks 中的setTimeout。更精细地看一个事件循环内的大致流程是执行同步代码 → 调用栈清空 → 清空 Microtasks 队列 → 浏览器可能执行渲染 → 执行下一个 Task。需要注意这里讨论的是一次 Event Loop 内立即执行的优先级与定时器的延迟时间如 3000ms、0ms不要混淆。4 实战精读componentWillMount 中异步 setState 的时灵时不灵理解了事件循环才能真正看懂 React 生命周期中那些看似诡异的行为。原文档作者在编写 dob-react 测试时发现componentWillMount函数在Microtask时机调用setState不会触发 rerender。4.1 只 render 一次的错误写法class Hello extends React.Component { async componentWillMount() { await immediate(() { this.setState({ a: 1 }); }); } render() { /**/ } }配套的immediate函数如下function immediate(fn) { return new Promise(resolve { fn(); resolve(); }); }这种写法下组件只会 render 一次。4.2 render 两次的正确写法如果再套一层Promise.resolve().then()让fn真正推迟到微任务时机执行function immediate(fn) { return new Promise(resolve Promise.resolve().then(() { fn(); resolve(); }) ); }此时组件会render 两次。4.3 原理与勘误原文档的 ps 补充非常重要第一种immediate写法其实是错误的——fn()在new Promise的 executor 中同步执行了await并没有真正推迟到微任务时机所以setState仍然发生在 React 生命周期内的同步渲染过程中setState被合并只触发一次 render。正确做法应使用Promise.resolve().then()将fn的调用真正推迟到 Microtask 时机。这个实验暴露了一个普遍问题在生命周期函数中异步setState的合并机制时而生效、时而不生效取决于setState被调用的时机落在同步执行阶段还是微任务阶段。这正是事件循环知识在真实业务中的价值——只有理解了 Microtask 的消费时机才能解释 React 为何合并状态更新以及何时合并会失效。4.4 从 React 实现看 setState 的调度入口要彻底理解上述现象可以顺藤摸瓜看 React 的状态更新链路。setState 做了什么精读 指出react包本身不包含 DOM 更新逻辑它只通过updater接口向渲染器react-dom、react-native等反向通信// A bit simplified setState(partialState, callback) { // Use the updater field to talk back to the renderer! this.updater.enqueueSetState(this, partialState, callback); }也就是说setState是否合并、何时触发 rerender最终由react-dom的渲染器在事件循环的某个时机同步任务还是微任务决定——这与本文事件循环的分析天然衔接。5 从事件循环到 React 调度与 Promise 工程实践事件循环并非孤立的面试知识点它向下支撑了框架的调度设计向上决定了业务异步代码的写法。5.1 React 的调度本质是事件循环上的时间分片Scheduling in React 精读 指出浏览器同一时间只能做一件事肉眼可识别的刷新频率约 60FPS意味着渲染、动画、响应用户输入必须在约 16ms 内完成。React 16 的 Concurrent 模式把同步渲染拆解为可中断的异步渲染利用浏览器事件循环的空闲分片分批执行任务本质上是站在事件循环之上做任务优先级调度。5.2 Promise 串行队列的底层原理用 Reduce 实现 Promise 串行执行精读 从另一个角度印证了事件循环模型reduce是同步执行的在一个事件循环内完成——它只是在内存中快速构造了一条 Promise 执行链function runPromiseByQueue(myPromises) { myPromises.reduce( (previousPromise, nextPromise) previousPromise.then(() nextPromise()), Promise.resolve() ); }reduce的作用就是把每个 Promise 完成后执行下一个的冗余队列代码折叠成一行生成。更现代的做法是用async/await改写为顺序等待async function runPromiseByQueue(myPromises) { for (let value of myPromises) { await value(); } }理解了两者的差异前者同步构造队列、后者异步逐项等待才能回答为什么Promise.all是并行、而await逐个写是串行这类问题。6 总结理解事件循环只是第一步。原文档作者坦言想写好稳健的业务代码需要三层能力理解内科知识Call Stack 只装同步代码异步代码进入 Event Loop 队列Microtask 优先于 Macrotask 消费读懂框架源码例如 React 的setState通过updater委托给渲染器实现调度与合并行为由渲染器在事件循环的时机决定保证不会忘异步时序知识是用到才想起来型知识建议通过类似本文第 4 节的可复现实验持续巩固。最后回到实践层面仓库中 Tasks, microtasks 精读 给出了两条务实建议业务逻辑不要巧妙依赖 Microtask 与 Task 执行顺序的微妙差异——不同浏览器Chrome、Firefox、Safari、Edge对任务顺序的实现存在差异依赖顺序的代码非常脆弱不要死记硬背调用顺序——只要记住核心规则即可推导一次事件循环中同步代码优先随后清空 Microtasks再处理 TasksMicrotasks 执行期间新插入的 Microtasks 会按序继续执行。类似的异步思维贯穿仓库多个主题处理异步异常的 捕获所有异步 error、并发场景下 async/await 是把双刃剑 中对顺序 await 拖慢并发的警示都可与本文的事件循环模型互相印证。当你下次面对为什么结果顺序不对为什么 setState 没生效时不妨先回到事件循环这个内科诊断一遍。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐把静态图片变成动态视频一文搞懂 ComfyUI-WanVideoWrapper把静态图片变成动态视频一文搞懂 ComfyUI WanVideoWrapper 第一次做 AI 视频时我的目标很具体手上有一张红衣男子的照片想让它转头、人工智能大模型媒体生成JavaScript事件循环终极指南深入理解异步编程运行机制JavaScript事件循环终极指南深入理解异步编程运行机制 JavaScript事件循环是理解现代JavaScript异步编程的关键概念。作为一名JavaS教程示例工程JavaScript事件循环终极指南深入理解异步编程运行时机制JavaScript事件循环终极指南深入理解异步编程运行时机制 你是否曾经好奇为什么 setTimeout 并不总是准时执行为什么 Promise 比回调更文档教程上一篇ncmdump使用手记网易云NCM格式转换从拖拽到自动批量一篇讲透下一篇3步搞定NCM格式转换用ncmdump让加密音乐跨设备自由播放创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑