当前位置: 首页 > news >正文

事件循环其实很简单!

一、概念

JavaScript 是单线程执行(基于执行栈 / 调用栈 call stack),事件循环负责不断地从各种任务队列里取任务执行——以保证异步任务的函数回调按规则有序运行,浏览器环境和 Node.js 环境都使用事件循环,尽管他们的事件循环逻辑并不相同。

之所以函数的执行基于“栈”这种结构,是因为 js 函数允许嵌套,先调用的函数需要等待内部函数的调用执行完毕才能执行,也就是先调用后执行的逻辑,正好满足“栈”这种数据结构。

执行栈/调用栈是针对函数调用来说的,而我们 js 任务的执行依赖于任务队列,先进入队列的任务会先执行,而且一个任务中可能存在多个函数。要注意一个是函数调用的机制,一个是任务执行的机制,不是一回事!

二、基本构件

  • Call Stack(调用栈)​:同步代码入栈执行、执行完出栈。
  • 宏任务(macrotask / task)队列​:例如 setTimeoutsetIntervalsetImmediate(Node)、DOM 事件、I/O 回调、UI 渲染触发等。

    宏任务作为之前的一种笼统叫法,现代浏览器对这些任务做了更细的划分,对他们都统称为了 task,不同的任务具有不同的队列。不过,微任务的概念一直被保留使用。

  • 微任务(microtask / job)队列​:例如 Promise.then/catch/finallyasync+awaitqueueMicrotaskMutationObserver(浏览器)、process.nextTick(Node)。
  • 渲染/绘制阶段(browser)​:在合适时机把更新绘制到屏幕(通常在 macrotask 完成并且 microtasks 已清空之后)。

    主要为了后面对于任务执行和浏览器渲染顺序的理解。

  • 事件循环(event loop)​:不断循环——执行一个 macrotask → 清空所有 microtasks → 执行渲染(若需要) → 下一个 macrotask。

三、浏览器里的执行模型

循环的每一轮(tick)大致顺序:

  1. macrotask queue 取出一个任务并执行(例如页面初始 script)。
  2. 当前任务执行完后,立即运行并清空 ​microtask queue​(每出现一个 microtask,它会被加入队列;直到队列空才返回)。

    microtasks 在同一轮里可能不断产生并被立即处理。

  3. 当 microtasks 清空后,会进行一次 ​渲染/绘制​(如果需要)。
  4. 进入下一轮 macrotask。

结论:​microtask 的优先级高于下一个 macrotask​。

这里多提一嘴“tick”,不知道有多少同学看到这个“tick”,马上就会联想到 Vue 中的 nextTick,其实,他们确实有一定渊源。

事件循环中的 tick :

tick = 一次事件循环的执行周期 = Task → Microtask → Render → 下一 tick

而 Vue.nextTick 作用试讲 DOM 更新后的回调放入微任务队列(或者退化为宏任务),主要是为了解决 DOM 的异步更新导致无法得到最新 DOM。Vue 源码逻辑:

if (Promise) microtask
else if (MutationObserver) microtask // 旧浏览器
else macrotask fallback // setImmediate(IE专属) -> setTimeout(Macrotask,最差)

Vue 官方文档对于 nextTick 的解释是:等待下一次 DOM 更新刷新的工具方法。和事件循环中的 tick 何其相似。

除此之外,对于浏览器渲染和事件循环结合很多同学没有了解过,以下是一个结合浏览器渲染的例子:

<script>
console.log('start');setTimeout(() => console.log('timeout'), 0);Promise.resolve().then(() => console.log('promise'));requestAnimationFrame(() => console.log('raf'));console.log('end');
</script>

在浏览器输出:start end promise raf timeout

解释:

  • 同步:startend
  • microtask: promise
  • 渲染相关:requestAnimationFrame 在下一帧渲染前执行(在 microtasks 清空后,但通常在 macrotask 之前的 render 时机),所以 raftimeout 之前
  • setTimeout 是下一轮 macrotask,所以最后输出。

四、Node.js(libuv)与浏览器的区别

Node 的底层是 libuv,事件循环分多个阶段:

  1. timers(处理 setTimeout/setInterval)
  2. pending callbacks(I/O 回调)
  3. idle, prepare(内部使用)
  4. poll(检索新的 I/O 事件并执行)
  5. check(处理 setImmediate)
  6. close callbacks(socket close 等)

微任务(Promise callbacks)是在每个阶段执行后 ​立即清空​(microtask checkpoint);另外 Node 有 process.nextTick,其优先级甚至高于 microtasks(会在当前阶段马上执行,且会在 Promise microtasks 之前运行)。

我不想放很多面试题去讲解,因为面试题是做不完的,而知识的核心重点就是这些。

上面的理论搞懂了,基本上相关面试题都可以做对。

http://www.jsqmd.com/news/43079/

相关文章:

  • 从0到1:揭秘LLM预训练前的海量数据清洗全流程
  • AI技术落地实践
  • Day22flex布局
  • CF2169A题解
  • re.compile为什么能提高速度?
  • 从 0 搭建 LLM 不再难!这个 PyTorch 项目帮你吃透大模型底层逻辑
  • 题解:P8819 [CSP-S 2022] 星战
  • instr在mysql索引中作用是什么
  • initrans参数在oracle高并发环境下的作用
  • Java集合之【CopyOnWrite和Collections.synchronizedList()的区别】
  • 20232324 2024-2025-1 《网络与系统攻防技术》实验六实验报告
  • Python调用C++代码
  • 复杂状态与数据流管理:分布式定时任务系统的设计
  • 【第6章 字符串】Python 字符串常用操作完全教程(含代码演示)
  • DAG-有向无环图-拓扑排序
  • MySQL EXPLAIN中的key_len:精准掌握索引使用情况
  • 1090 : 分解因数 25-11-17
  • NOIP 模拟赛 9
  • Sora 2 Cameo多角色上传+Remix二创功能API接入教程,史低0.08/条
  • info linux
  • AWS云服务深度集成
  • httpd linux 启动
  • 浅谈 Manacher
  • 第28天(简单题中等题 二分查找)
  • 基于MIMO系统的SCMA稀疏码多址接入和MPA消息传递算法matlab仿真
  • Node.js服务稳定性保障:从热更新到高可用体系
  • 一次尝试,3个小时90元的主机游玩和F1电影
  • NOIP 模拟赛 8
  • 静态路由的配置
  • 读书笔记:“外部表”的进阶使用,它主要解决了三个核心问题:如何切换文件、多用户怎么办,以及一个非常酷的玩法——把系统命令变成表。