浏览器事件循环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主线程空闲后,会去查询队列中是否还有任务,开启新一轮的循环。

这个步骤我大概画了个图:

浏览器事件循环Event Loop

现在我们照着最开始的面试题进行举例。

首先,这段代码是一整个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中异步的具体实现方式,它的实现需要来自宿主环境的支持,比如浏览器中的各种线程,运行时中的消息队列等等。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件举报,一经查实,本站将立刻删除。

本文链接:https://www.f2er.com/3188545.html

大家都在看

  • 飞码LowCode前端技术系列:如何便捷快速验证实现投产及飞码探索

    本篇文章从数据中心,事件中心如何协议工作、不依赖环境对vue2.x、vue3.x都可以支持、投产页面问题定位三个方面进行分析。
    2023-11-16 博文
  • 如何优雅使用 vuex

    大纲 本文内容更多的是讲讲使用 vuex 的一些心得想法,所以大概会讲述下面这些点: Q1:我为什么会想使用 vuex 来管理数据状态交互? Q2:使用 vuex 框架有哪些缺点或者说副作用? Q3:我是如何在项目里使用 vuex 的? 初识 vuex 对于 vuex,有人喜欢,有人反感 喜欢的人觉
    2023-11-16 博文
  • 第三方组件及计算属性传参的问题解决方式

    1. 前言 唉,好想玩滋嘣。 2. 计算属性直接传参接收不到 表格数据某一列需要用的计算属性时,模板中使用计算属性 fullName 就会直接调用 fullName 函数,而在模板中 fullName(item) 相当于fullName()(item),此处为函数柯里化。 <el-table-
    2023-11-16 博文
  • 记录--Vue3基于Grid布局简单实现一个瀑布流组件

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前言 在学习Grid布局之时,我发现其是CSS中的一种强大的布局方案,它将网页划分成一个个网格,可以任意组合不同的网格,做出各种各样的布局,在刷某书和某宝首页时,我们发现其展示方式就是一种瀑布流,是一种流行的网站页面布局,视觉表……
    2023-11-16 博文
  • 用强数据类型保护你的表单数据-基于antd表单的类型约束

    接口数据类型与表单提交数据类型,在大多数情况下,大部分属性的类型是相同的,但很少能做到完全统一。我在之前的工作中经常为了方便,直接将接口数据类型复用为表单内数据类型,在遇到属性类型不一致的情况时会使用any强制忽略类型错误。后来经过自省与思考,这种工作模式会引起各种隐藏bug,一定有更……
    2023-11-16 博文
  • pinia的使用

    前言 最近新开了个项目,以前老项目都是vue2+vuex开发的,都说用vue3+pinia爽得多,那新项目就vue3+pinia吧。这里记录一下pinia的使用。 使用方法 安装pinia: npm i pinia main.js中引入pinia: //main.js import { create
    2023-11-16 博文
  • 记录--让我们来深入了解一下前端“三清”是什么

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前端“三清” 在前端开发中,我们经常听到关于“三清”的说法,即 window、document、Object。这三者分别代表了 BOM(浏览器对象模型)、DOM(文档对象模型)以及 JS 的顶层对象。在这个体系中,我们通过 JavaScr
    2023-11-16 博文
  • 记录--啊?Vue是有三种路由模式的?

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 众所周知,vue路由模式常见的有 history 和 hash 模式,但其实还有一种方式-abstract模式(了解一哈~) 别急,本文我们将重点逐步了解: 路由 + 几种路由模式 + 使用场景 + 思考 + freestyle 路由概念
    2023-11-16 博文