浏览器事件循环Event Loop
引言:
事件循环不是浏览器独有的,从字面上看,“循环”可以简单地认为就是重复,比如for循环,就是重复地执行for循环体中的语句,所以事件循环,可以理解为重复地处理事件,那么下一个问题是,处理的是什么事件,事件的相关信息从哪里获取。
因为我没有用nodejs做过什么项目,所以这里我暂且只关注浏览器的事件循环,但我想就“事件循环”本身而言,原理应该是相同的,不过就具体的实现可能存在一些差异。
一道面试题
相信应该有部分小伙伴和我一样,在面试中曾遇到过类似于这种问打印结果的题目。
(async function main() {
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
setTimeout(() => {
console.log(3);
}, 100);
let p1 = new Promise((resolve, reject) => {
console.log(4);
resolve(5);
console.log(6);
});
p1.then((res) => {
console.log(res);
});
let result = await Promise.resolve(7);
console.log(result);
console.log(8);
})()
这种题目就是变相的在考察事件循环的知识。
我个人感觉事件循环这个点,也是随着Promise的出现,成为了一个常见的考点。
什么是事件循环
一提到事件循环,我想很多人会和我一样,立刻想到异步、宏任务、微任务什么的。
WIKI
先不着急,我们先看下Wiki上,对事件循环的通用性描述。
In computer science, the event loop is a programming construct or design pattern that waits for and dispatches events or messages in a program. The event loop works by making a request to some internal or external "event provider" (that generally blocks the request until an event has arrived), then calls the relevant event handler ("dispatches the event"). The event loop is also sometimes referred to as the message dispatcher, message loop, message pump, or run loop.
The event-loop may be used in conjunction with a reactor, if the event provider follows the file interface, which can be selected or 'polled' (the Unix system call, not actual polling). The event loop almost always operates asynchronously with the message originator.
When the event loop forms the central control flow construct of a program, as it often does, it may be termed the main loop or main event loop. This title is appropriate, because such an event loop is at the highest level of control within the program.
简而言之,事件循环是一种编程结构或设计模式,用于在程序中等待和派发事件或消息。
它的工作原理是,向内部或外部的“事件提供者”发出请求(通常会阻止请求,直到事件发生)这就回答了我们之前的问题:事件的信息从哪里来,是由“事件提供者”提供,然后调用相关的事件处理程序(“派发事件”)关于如何处理事件。
事件循环有时也被称为消息派发器、消息循环、消息泵或者运行循环。
事件循环几乎总是与消息发送者异步运行。
这里我觉得可以这么理解,“消息发送者”这边将事件的消息交给了“事件提供者”,而事件循环这边会向“事件提供者”发出请求获取事件,然后调用相关的事件处理程序;所以说,事件循环与消息发送者是异步运行。
事件循环必然是在“消息发送者”将事件的消息交出之后,才会去执行事件处理程序;也就是说,事件循环的操作是在当下之后,在”将来“才会发生的。
当事件循环构成程序的中心控制流结构时(通常如此),它可以被称为主循环或主事件循环。这个称谓是恰当的,因为这样的事件循环处于程序的最高控制层。
MDN
WIKI上提供的是通用性的描述。我们再看一下MDN,MDN上直接搜索事件循环,可以看到是位于JavaScript路径下,针对JavaScript事件循环的描述。
JavaScript has a runtime model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks. This model is quite different from models in other languages like C and Java.
第一段很直白的描述:JavaScript的运行时模型,是基于事件循环的,负责执行代码、收集和处理事件以及执行队列中的子任务。
执行队列中的子任务:这基本上等于说,JavaScript的运行时给JavaScript提供了事件循环的能力;可以说JavaScript运行时中事件循环的部分,提供了JavaScript异步的具体实现方式。给JavaScript提供了支持异步的能力。
那么JavaScript为什么要处理异步呢?这就不得不提JavaScript的单线程运行特性 ,线程是什么?是进行运算调度的最小单位,而JavaScript设计之初,是为了处理网页上的交互事件,如果JavaScript允许多线程,也就是允许多个触发的事件同时进行运算,这可能就会呈现出各种不一样的计算结果,在用户看来就会显得交互很混乱,为了减少不确定性,JavaScript干脆就选择了单线程运行,所有代码都在同一个线程中执行;另外,JavaScript中的交互事件很多,如果每个触发事件都单独开辟线程来处理,也是不小的开销吧。
但是呢,虽然JavaScript是单线程运行的,但也存在需要在将来完成的操作,也就是存在异步代码,比如定时器。如果在Java中,我们也许可以选择new一个线程,sleep多少秒,然后再执行,但是JavaScript中不能这样做,因为它没有多线程,而如果直接在主线程等待,必定会引发阻塞和卡顿。事件循环就是对这种情况的一种解决方案,为了协调浏览器中的各种事件,必须使用事件循环;而事件循环中的消息队列就由JavaScript运行时来管理。
运行时概念
相信不少前端同学都听过“运行时”这个词,那运行时到底是什么呢?我觉得可以这么简单理解,既然运行时的功能是负责执行代码、收集和处理事件以及执行队列中的子任务,那么运行时中必须定义一套规则,关于如何去处理这些事情。所以可以简单地把运行时认为是定义了一套执行规则的JavaScript执行环境。
关于运行时,可以看到MDN上有一个直观演示的图,其中包含了函数调用形成的执行栈、分配对象的堆,以及消息队列。
根据WIKI给出的描述,运行时模型中,与事件循环关系最密切的,是消息队列,也就是我们前面提到的“事件提供者”。现在我们来看这个队列。
A JavaScript runtime uses a message queue, which is a list of messages to be processed. Each message has an associated function that gets called to handle the message.
At some point during the event loop, the runtime starts handling the messages on the queue, starting with the oldest one. To do so, the message is removed from the queue and its corresponding function is called with the message as an input parameter. As always, calling a function creates a new stack frame for that function's use.
The processing of functions continues until the stack is once again empty. Then, the event loop will process the next message in the queue (if there is one).
我们来看翻译的内容:
JavaScript 运行时使用消息队列,这是一个待处理消息列表。每条消息都有一个相关函数被调用来处理该消息。
在事件循环中的某个时刻,运行时开始处理队列中的消息,从最旧的消息开始。(”队“这个数据结构我们知道,是先进先出的,所以先进队的消息会先被处理。)为此,会从队列中移除消息,并将消息作为输入参数调用相应的函数。一如既往,调用函数会创建一个新的堆栈框架供该函数使用。
函数的处理将一直持续到堆栈再次清空为止。然后,事件循环将处理队列中的下一条消息(如果有的话)。(也就是,消息队列中的消息是一条接一条处理的。这里的堆栈指的就是函数调用形成的执行栈和分配对象的堆)
那么队列中的消息是哪里来的呢? 从这段内容中我们可以知道,进队的消息已经在等待处理了;所以比如有个定时器setTimeout,定义了有段代码需要等待3秒才执行,那这段代码就不能直接就进队,为了保证动作3秒后才执行,会在3秒后才进队,也就是说,setTimeout的第二个参数代表的是将消息推入队列的延迟时间。
那么肯定需要有什么东西,来管理这段代码,将这段代码在给定的延时后,推入消息队列。既然js没法去开线程管理,所以也是浏览器在管理;Chrome就有一个定时器线程,专门用于处理定时器,在定时器计时结束后,通知事件触发线程将消息推入队列;同样的,在用户触发交互事件时,事件触发线程也会将已在代码中定义的消息推入队列,也就是在事件监听程序addEventListener中监听的操作;还有异步HTTP请求线程,来管理请求回调的消息入队。等等,浏览器的这些线程共同作用来实现事件循环这个机制。
在JS主线程空闲时,就会将这些消息队列中的消息出列,交由主线程来执行。
那么接下来就是事件循环的执行步骤的问题。
事件循环执行步骤
首先,关于微任务:我们来看HTML的文档。
Each event loop has a microtask queue, which is a queue of microtasks, initially empty. A microtask is a colloquial way of referring to a task that was created via the queue a microtask algorithm.
每个事件循环都有一个微任务队列,这是一个初始为空的微任务队列。微任务是一种通俗的说法,指通过微任务队列算法创建的任务。
也就是说,每个事件循环都会维护一个自己的微任务队列。它和我们之前看的消息队列,不是同一个队列,消息队列指的是这个文档中的任务队列,也就是task queue。
总所周知,常见的产生宏任务的方式有script、setTimeout、setInterval、UI事件等等;常见的产生微任务的方式有Promise.prototype.then、MutationObserver等等。
假设我们在浏览器中加载了一个页面,现在我们来看事件循环的处理步骤:
- 初始状态:运行时的调用栈空。微任务队列空,消息队列里有且仅有一个script脚本(整体代码)
- 然后消息队列中的script脚本被推入调用栈,同步代码开始执行。
- 当碰到微任务时,比如Promise.then,就将微任务推入事件循环的微任务队列中;这里要注意一下,Promise执行器函数中的代码属于同步代码,会被顺序执行;
- 当碰到宏任务时,就将它们丢给相应的浏览器线程;
- 当本次代码中的同步代码都执行完毕后,就将微任务队列中的任务一一处理并出队;
- 这样就完成了一次循环;
- 本次的宏任务script脚本也被出队。
- 此时DOM修改完成,然后浏览器会执行渲染操作,更新界面。
- 如果宏任务在各自的线程中被处理完毕后,就会被推入消息队列。
- 再接着就是当JS主线程空闲后,会去查询队列中是否还有任务,开启新一轮的循环。
这个步骤我大概画了个图:

现在我们照着最开始的面试题进行举例。
首先,这段代码是一整个script脚本,其中的同步代码会首先被按顺序执行,
可以看到这个script脚本中有一个async异步函数,async函数中的同步代码会首先被执行,所以先会打印1,
然后碰到两个产生宏任务的setTimeout,丢给定时器线程,为了后面方便讲述,这里分别把它们叫做宏任务1和宏任务2;
然后执行promise执行器函数中的同步代码,打印4和6;
接着碰到Promise.then这个微任务,我们给它记为微任务1,将它推入微任务队列,
然后我们又碰到一个await,await之后的代码相当于是Promise.then中的代码,也就是会被推入微任务队列,我们给它记为微任务2;
到这里,本次循环中的同步代码都执行完毕了;
接着就是开始把微任务队列中的微任务取出执行,首先是执行微任务1,打印5;
接着执行微任务2,打印7和8;
本次事件循环就结束了。
等到计时结束,宏任务1会先被推入消息队列,在JS主线程空闲,去查询消息队列后,代码就会被执行,会打印2;
同理,宏任务2后面也会被执行,并打印3。
这样我们就完成了这道面试题的解答。
总结
总的来说,事件循环就是JS中异步的具体实现方式,它的实现需要来自宿主环境的支持,比如浏览器中的各种线程,运行时中的消息队列等等。
浏览器事件循环Event Loop的更多相关文章
- JS事件循环(Event Loop)机制
前言 众所周知,为了与浏览器进行交互,Javascript是一门非阻塞单线程脚本语言. 为何单线程? 因为如果在DOM操作中,有两个线程一个添加节点,一个删除节点,浏览器并不知道以哪个为准,所以只能选 ...
- 事件循环 event loop 究竟是什么
事件循环 event loop 究竟是什么 一些概念 浏览器运行时是多进程,从任务管理器或者活动监视器上可以验证. 打开新标签页和增加一个插件都会增加一个进程,如下图:  浏览器渲染进程是多线程,包 ...
- 事件循环Event loop到底是什么
摘要:本文通过结合官方文档MDN和其他博客深入解析浏览器的事件循环机制,而NodeJS有另一套事件循环机制,不在本文讨论范围中.process.nextTick和setImmediate是NodeJS ...
- 简单了解一下事件循环(Event Loop)
关于我 一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android.Python.Java和Go,这个也是我们团队的主要技术栈. Github:https:/ ...
- 浏览器与Node的事件循环(Event Loop)有何区别?
前言 本文我们将会介绍 JS 实现异步的原理,并且了解了在浏览器和 Node 中 Event Loop 其实是不相同的. 一.线程与进程 1. 概念 我们经常说 JS 是单线程执行的,指的是一个进程里 ...
- JavaScript事件循环(Event Loop)机制
JavaScript 是单线程单并发语言 什么是单线程 主程序只有一个线程,即同一时间片断内其只能执行单个任务. 为什么选择单线程? JavaScript的主要用途是与用户互动,以及操作DOM.这决定 ...
- JavaScipt 中的事件循环(event loop),以及微任务 和宏任务的概念
说事件循环(event loop)之前先要搞清楚几个问题. 1. js为什么是单线程的? 试想一下,如果js不是单线程的,同时有两个方法作用dom,一个删除,一个修改,那么这时候浏览器该听谁的? ...
- JavaScript 事件循环 — event loop
引言 相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 .在初期许多人会把异步理解成类似多线程的编程模式 ...
- 一文梳理JavaScript 事件循环(Event Loop)
事件循环(Event Loop),是每个JS开发者都会接触到的概念,但是刚接触时可能会存在各种疑惑. 众所周知,JS是单线程的,即同一时间只能运行一个任务.一般情况下这不会引发问题,但是如果我们有一个 ...
- 事件循环Event Loop
在 事件循环 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息.被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数.正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧. ...
随机推荐
- 开源元数据管理平台Datahub最新版本0.10.5——安装部署手册(附离线安装包)
大家好,我是独孤风. 开源元数据管理平台Datahub近期得到了飞速的发展.已经更新到了0.10.5的版本,来咨询我的小伙伴也越来越多,特别是安装过程有很多问题.本文经过和群里大伙伴的共同讨论,总结出 ...
- [python]enumerate迭代
Python中有个内置的函数叫做 enumerate,可以在迭代时返回元素的索引. # 示例代码01 warframe = ["saryn", "wisp", ...
- 【技术积累】Linux中的命令行【理论篇】【七】
atrm命令 命令介绍 atrm命令是Linux系统中的一个命令行工具,用于取消或删除已经安排的at命令.at命令是一种用于在指定时间执行一次性任务的工具. 命令说明 atrm命令的语法如下: atr ...
- IDA的使用2
IDA的使用2 string类型的选择 Rename 要注意如果再namelist和public name里面是不能重名 操作数 这个主要和开发结合精密, change sign-改变符号 bitwi ...
- Unity 编辑器资源导入处理函数 OnPreprocessTexture:深入解析与实用案例
Unity 编辑器资源导入处理函数 OnPreprocessTexture 用法 点击封面跳转下载页面 简介 在Unity中,我们可以使用编辑器资源导入处理函数(OnPreprocessTexture ...
- python 面试题第一弹
1. 如何理解Python中的深浅拷贝 浅拷贝(Shallow Copy)创建一个新的对象,该对象的内容是原始对象的引用.这意味着新对象与原始对象共享相同的内存地址,因此对于可变对象来说,如果修改了其 ...
- API接口设计规范
说明:在实际的业务中,难免会跟第三方系统进行数据的交互与传递,那么如何保证数据在传输过程中的安全呢(防窃取)?除了https的协议之外,能不能加上通用的一套算法以及规范来保证传输的安全性呢? 下面我们 ...
- API接口获取快手商品详情(封装代码)
快手是中国最大的短视频平台之一,也是许多电商企业进行推广的重要渠道.为了更好地了解快手的商品信息,我们可以通过API接口来获取商品详情. 首先,我们需要了解快手API接口和相应的文档 接下来,我们需要 ...
- WPF学习 - 用鼠标移动、缩放、旋转图片(1)
1. 需求 其实我的需求很简单.就是想做一个图片查看器,可以通过鼠标来平移.缩放.旋转图片. 2. 解决思路: WPF中的UIElement提供了RenderTransform属性,用于承载各种Tra ...
- 从Google开发者大会浅谈LLM的应用
这周参加了在上海世博中心举办Google I/O Connect中国开发者大会,有几年没参加这么高质量的活动,有点感慨. 期间重点听了关于GCP和Google AI大语言模型的主题演讲,发现目前各大厂 ...