认识Event Loop【1】

前言

这应该是一个系列文章,因为我觉得Event Loop(事件循环)是一件很抽象也很重要的一个机制。eventloop这个知识点处于非常杂糅的位置,和很多其他知识,如运行时、浏览器、渲染流程、数据结构、线程等等,也是正确理解JS异步编程的核心。所以很值得展开来娓娓道来。

⚠️强烈建议学完JS异步语法知识后再来学习EventLoop。

JS是单线程的编程语言

JS是单线程的编程语言,重要的事情说三遍,单线程,单线程,单线程 。接下来请读者思考一下,单线程是什么意思? 单线程意味着JavaScript在同一时刻只能执行一个任务或操作 。也就是说,它一次只能处理一个事件或指令,不会同时处理多个任务 。 那我就不禁要问,单线程如何实现异步编程,这不是开玩笑吗,how,轮询?回调?这不麻烦死了。能实现基本操作还好,但有稍微复杂点的该如何呢。举一个例子,setTimeout的时候为什么还可以执行其他代码,谁在那边倒数时间?执行其他任务和进行记时任务,这俩件事情是如何在所谓单线程的JS中同时执行的。蒙了吗,我就不禁要怀疑JS是不是单线程了。 直到我去了解了EventLoop,EventLoop就是在保证JS是单线程的特点的同时,让其拥有高效实现复杂异步的能力。 Eventloop避免了阻塞和性能瓶颈。如果没有事件循环,虽然理论上可以通过轮询、回调等方式实现异步编程,但这些方式都存在效率低、开发复杂度高等问题,且无法像事件循环那样高效地协调和管理多个异步操作。 简单介绍完了Event Loop的重要性,那么我们要开始正式认识它了。

Runtime运行时

我先说结论,JavaScript 本身是单线程的 ,但它的 运行环境(runtime) (如浏览器或 Node.js)提供了 事件循环(Event Loop) ,并通过调用 底层的多线程 API (如浏览器的 Web APIs 或 Node.js 的 libuv 线程池)来实现异步操作。 那为什么浏览器能运行JS代码,就是因为浏览器内置了解析JS代码的工具,以Chromium浏览器内核为例,就是使用了大名鼎鼎的V8引擎。而eventloop实际上和runtime关系更密切的,本文就先以浏览器的Event Loop为中心,理解完浏览器后,下一章再介绍nodejs、deno、bun这些其他的"Runtime"

浏览器提供的runtime

看这幅图,大概就明白JS引擎和运行时的关系了吧,它们之间是包含关系,而EventLoop并不是在JS引擎里实现的。 我们先不关注EventLoop的具体细节,乘胜追击简单了解一下浏览器的架构。

浏览器是多进程+多线程架构的

现代Chorme浏览器是多进程架构,一个页面就是一个进程,而一个进程可以有多个线程。通过多个线程协作来完成不同的任务,包括 JavaScript 执行、页面渲染、I/O 操作等。通常,以下是常见的几个关键线程:

  • 主线程:负责执行 JavaScript 代码、渲染页面、响应用户输入等。它与事件循环机制紧密配合。
  • 渲染线程(Rendering Thread) :专门负责页面渲染。它从 DOM 和 CSSOM 构建渲染树(Render Tree),然后进行页面布局、绘制和合成。渲染线程会独立于主线程运行,但也需要与主线程进行一定的交互(例如,当 DOM 更新时,渲染线程需要重新绘制)。
  • 网络线程(Network Thread) :负责处理网络请求,例如发起 HTTP 请求(如 fetchXMLHttpRequest)以及接收服务器响应。它通常与主线程和渲染线程协作,但它运行在独立的线程中,不会阻塞 JavaScript 的执行。
  • I/O 线程(I/O Thread) :处理与操作系统相关的 I/O 操作(如文件操作、网络操作等)。它的作用是将 I/O 操作的阻塞部分从主线程中分离出来,避免长时间的阻塞导致浏览器卡顿。
  • Web Worker:Web Worker 是一个额外的线程,通常用于执行耗时的计算任务,以防止阻塞主线程。Web Worker 线程之间通过消息传递与主线程或其他 Worker 进行通信。

有了宏观的视角,读者应该能猜出来了吧,JS就是通过调用WebAPIs,将I/O、Network、Render等极有可能消耗时间的任务甩锅给了浏览器的其他线程去执行,真坏啊,摸鱼技能+1。

理解为啥会堵塞

咱们先理解单核CPU,单核CPU其实就类似于在一个在单一轨道上行驶的超跑,在闲置的时间(EventQueue和RenderTask没任务,这俩都是我自己乱取名的嗷),就一直在黑色的轨道上一圈圈慢慢的跑。 当任务来了,需要CPU去进行处理的时候,这辆跑车就会进入蓝色或黄色轨道,去进行计算或读写操作。可能会对为什么CPU要去处理渲染任务有疑惑,这点和浏览器的渲染流程有关系,我直接简短告诉结论:渲染需要CPU先进行运算操作,后交由GPU去进行后续的处理,即需要二者配合,没有CPU的参与就会出现渲染堵塞的问题 。 接下来我们在把注意力集中在EventQueue 上,EventQueue,我自己这么叫它的,顾名思义,就是以类似于排队的感觉。各种事件和任务一个一个的进入队列中,然后等待CPU处理,处理完就滚蛋了,处理不完就会一直拖着CPU,不让它离开,像个事佬,其他任务处理不了,只能等着,这就是所谓的堵塞

任务从哪来

让我们一窥全貌,call me灵魂画师🫡👍 接下来的重点就是左上角的那部分

两种数据结构

JS引擎维护一个CallStack回调栈,顾名思义,就一个栈结构,遵循先进后出(FILO)。而Runtime运行时维护一类TaskQueue,顾名思义,就是一个队列结构,遵循先进先出(FIFO)。 动画模拟感兴趣的可以看看这网站

CallStack

先关注CallStack,又是顾名思义啊,为什么叫Call,其实就是执行回调函数(CallBack)。这点其实不是我们这片文章的重点,简单的讲一下,就是应该是所有编程语言吧,同步代码都是以回调栈这种形式去执行代码。 其实我感觉,可以比较粗暴的讲一切操作理解为"同步",因为以回调栈的视角去看,无非是有任务来了就按照Stack的形式(FILO)去执行操作回调操作。

WebAPIs

developer.mozilla.org/en-US/docs/... developer.mozilla.org/zh-CN/docs/... WebAPIs有很多,感兴趣的可以自己去详细了解,我摘记了一段MDN的介绍

客户端 JavaScript 中有很多可用的 API。它们本身并不属于 JavaScript 语言,却建立在核心 JavaScript 语言之上,为使用 JavaScript 代码提供额外的超强能力。它们通常分为两类:

  • 浏览器 API 内置于 Web 浏览器中,能从浏览器和电脑周边环境中提取数据,并用来做有用的复杂的事情。例如,Web 音频 API 为在浏览器中处理音频提供了 JavaScript 结构------获取音轨、改变音量、应用特效等。在后台,浏览器实际上使用了一些复杂的低级代码(如 C++ 或 Rust)来进行实际的音频处理。但同样,这种复杂性已被应用程序接口抽象化。
  • 第三方 API 缺省情况下不会内置于浏览器中,通常必须在 Web 中的某个地方获取代码和信息。例如,Google Maps API 使你能够执行诸如在网站上显示办公室的交互式地图之类的操作。它提供一系列特殊的结构,可以用来查询 Google 地图服务并返回特定信息。

看看就好,这也提醒了我们,像是我们常用的DOM操作、fetch、setTimeout、setInterval都是通过WebAPIs来实现的,也就是说这些功能在其他Runtime中就不一定有,比如在NodeJS中就没有DOM操作。

TaskQueue

接下来就到了任务队列了 任务队列在浏览器环境中又区分微任务队列和宏任务队列,之所以区分,我猜测是为了对事件有更加精细的操作,就如NodeJS则甚至会有6个不同的阶段,当然NodeJS还不是本文的重点。

宏任务(Macro-task)

宏任务指的是宿主环境(如浏览器或 Node.js)提供的任务,通常与用户交互、I/O、定时器等相关。 通常是一些消耗时间比较多,计算量比较大的任务。

常见的宏任务

  • setTimeout
  • setInterval
  • setImmediate(Node.js 专有)
  • I/O 操作(如文件读取、数据库查询)

比较特殊的宏任务

  • requestAnimationFrame(浏览器专有)
  • UI 渲染任务(浏览器) 之所以说特殊,因为UI渲染的频率是和屏幕的刷新频率有直接关系的,也就是说屏幕如果说60hz的,在主线程不堵塞的情况下,就会每1/60秒去执行一次渲染任务,而如果个任务堵塞时间过长,就会将渲染任务延迟,甚至直接丢失(也就是会发生所谓的掉帧、丢帧)

特点

每次事件循环(Event Loop)都会从任务队列(Task Queue)中取出一个宏任务执行。

微任务(Micro-task)

常见的微任务

  • Promise.then()catch()finally()
  • MutationObserver(浏览器专有)
  • queueMicrotask()(现代浏览器和 Node.js)

特点

微任务的优先级高于宏任务,JS 引擎会立即清空所有的微任务队列(Micro-task Queue)中的任务。

执行顺序

已经了解了CallStack 和 TaskQueue,那么了解EventLoop就是易如反掌,接下来死记硬背一下规则就行了,没有为什么,就是一个人为规定的固定顺序

  1. 执行同步代码(包括变量声明、函数调用等)
  2. 执行所有微任务
  3. 取出并执行一个宏任务
  4. 重复步骤 2 和 3 这就是浏览器里的EventLoop

重新理解堵塞

微微的callback一下,从执行循序可以知道, 1.如果同步代码中执行的任务计算量太大,就会难以清空CallStack,堵塞TaskQueue里异步任务的执行。 2.如果微任务过多,就会堵塞宏任务的执行 3.如果单个任务执行时间超过两倍屏幕刷新率就会出现丢帧

后记

下次分享我应该会分享一下NodeJS的EventLoop,如果我时间的话,会顺手讲解一下Deno和Bun的EventLoop,当然这俩个目前没啥必要学,只是为了加深一下读者们"EventLoop由Runtime构建的而不是JS引擎"的这个重要观点。

相关推荐
涵信几秒前
第九节:性能优化高频题-首屏加载优化策略
前端·vue.js·性能优化
前端小巷子12 分钟前
CSS单位完全指南
前端·css
SunTecTec1 小时前
Flink Docker Application Mode 命令解析 - 修改命令以启用 Web UI
大数据·前端·docker·flink
拉不动的猪2 小时前
前端常见数组分析
前端·javascript·面试
小吕学编程2 小时前
ES练习册
java·前端·elasticsearch
Asthenia04122 小时前
Netty编解码器详解与实战
前端
袁煦丞2 小时前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
一个专注写代码的程序媛3 小时前
vue组件间通信
前端·javascript·vue.js
一笑code4 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员4 小时前
layui时间范围
前端·javascript·layui