0


WHAT - 前端事件循环(Event Loop)机制

目录

一、介绍

JavaScript 语言采用事件循环机制,来解决单线程运行带来的一些问题。

我们在下面将了解到 JavaScript 采用单线程机制,开发者无需考虑线程同步、锁、死锁等复杂的多线程编程问题。那如果所有的同步任务和异步任务,最终都会等待 JavaScript 主线程去处理,如何让这些任务有序地被处理呢?这就是我们今天要阐述的内容。

二、阐述

2.1 代理

在执行 JavaScript 代码时,JavaScript 运行时实际上维护了一组用于执行 JavaScript 代码的代理,每个代理由一组执行上下文的集合、执行上下文栈、主线程、一组可能创建用于执行 worker 的额外的线程集合、一个任务队列、一个微任务队列构成。

(1) 执行上下文集合

JavaScript 运行时维护了多个执行上下文,每个执行上下文对应着代码的执行环境,包括变量、作用域链等信息。

(2) 执行上下文栈

执行上下文栈是一种后进先出(LIFO)的数据结构,用于管理 JavaScript 代码的执行顺序。每当进入一个新的函数调用时,就会创建一个新的执行上下文,并被推入执行上下文栈中;当函数执行完毕后,其对应的执行上下文会被弹出栈。

(3) 主线程

JavaScript 代码的执行主要在单线程中进行,这个单线程就是主线程。主线程负责执行 JavaScript 代码,处理事件、执行任务队列中的任务等。

(4) 执行 worker 的线程集合

JavaScript 运行时还支持 Web Workers,这是一种在后台运行 JavaScript 代码的机制,可以创建额外的线程来执行 JavaScript 代码,以提高性能和响应能力。

(5) 一个任务队列

任务队列是一种队列数据结构,用于存储待执行的 JavaScript 任务。当 JavaScript 代码中遇到异步操作时(如定时器、事件监听器等),相关的任务会被放入任务队列中等待执行。

(6) 一个微任务队列

微任务队列用于存储微任务,微任务通常是一些需要在当前任务执行结束后尽快执行的任务,例如 Promise 的回调函数。微任务队列的执行优先级高于任务队列。

注意,除了主线程(某些浏览器在多个代理之间共享的主线程)之外,其他组成部分对该代理都是唯一的。

2.2 进程与线程

非常经典的一句话,进程是 CPU 资源分配的最小单位,线程是 CPU 调度的最小单位。

进程之间不共享资源,所以不会存在太多的安全问题,如浏览器中一个 tab 页或一个插件崩溃,也不会影响其他 tab 页应用和浏览器的运行。

线程是建立在进程的基础上的一个程序运行单位,一个进程可以有多个线程,同一个进程下的线程共享进程所拥有的资源和地址空间。

重点来了,对于现代浏览器,主要包括 5 类进程:

1. 浏览器进程(Browser Process)

这是整个浏览器的主进程,负责管理用户界面、各个 tab 页面的管理和协调、插件管理、网络资源加载等。每次启动浏览器时都会启动一个浏览器进程。

2. 渲染进程(Renderer Process)

每个标签页或者单独的窗口都有一个独立的渲染进程。它负责将 HTML、CSS 和 JavaScript 转换成用户可以交互的界面,同时也负责处理 JavaScript 的执行、布局和渲染等任务。

3. GPU 进程(GPU Process)

这个进程负责处理浏览器中与 GPU 相关的任务,例如对页面的 3D 绘制、加速视频解码等。它独立于浏览器进程和渲染进程,通过进程间通信(IPC)来协调任务。

进程间通信(IPC):指不同进程之间进行数据交换和通信的机制。在操作系统中,每个进程都是独立运行的,拥有自己的内存空间和资源,但有时候需要让不同的进程之间进行数据交换、共享资源或者协同工作。这时就需要使用 IPC 机制。常见的 IPC 机制包括:管道(Pipe)、消息队列(Message Queue)、信号量(Semaphore)、共享内存(Shared Memory)、套接字(Socket)。

5. 网络进程(Network Process)

这个进程负责处理网络请求和响应,包括 DNS 查询、建立和管理连接、安全认证等。它也独立于浏览器进程和渲染进程,通过 IPC 与其他进程通信。

5. 插件进程(Plugin Process)

如果网页使用了插件(如 Flash、Java 等),每个插件都会有一个独立的插件进程。这些进程通常由浏览器进程来创建和管理。

2.3 渲染进程中的线程

在渲染进程中,通常会有多个线程协同工作以实现页面的渲染和交互。这些线程包括:

1. 主线程(Main Thread)

主线程负责解析 HTML、CSS 和 JavaScript,构建 DOM 树和 CSSOM 树,并执行 JavaScript 代码。它还负责处理用户输入、渲染页面内容,并与其他线程进行通信。

在 Google 浏览器中这个主线程就是 V8 引擎。V8 引擎是 Chrome 浏览器中的 JavaScript 引擎,负责解释和执行 JavaScript 代码。

2. 渲染线程(Rendering Thread)

渲染线程负责将 DOM 树和 CSSOM 树转换为可视化的页面内容。它负责布局(Layout)、绘制(Painting)和合成(Compositing)页面元素,并将结果提交到显示器进行显示。

注意,主线程仅负责解析 html、css 构建 dom 树和 cssom 树。

3. 事件线程(Event Thread)

事件线程负责处理用户输入和其他事件,例如鼠标点击、键盘输入、网络请求完成等。它将事件推送到主线程进行处理。

4. 定时器线程(Timer Thread)

定时器线程负责管理 JavaScript 中的定时器和计时器,以确保定时器能够按时触发相应的回调函数。

5. 异步请求线程(Async Request Thread)

异步请求线程负责处理 JavaScript 中的异步操作,例如 XMLHttpRequest、fetch 等网络请求和其他异步任务。

这些线程之间通过什么机制来实现通信和协调的?消息队列和事件循环机制。

2.4 同步任务与异步任务

在 JavaScript 中,事件有同步与异步之分。在 JavaScript 中,常见的同步和异步事件包括:

同步事件:在主线程上排队执行的任务

  1. 代码执行:JavaScript 代码通常按照从上到下的顺序同步执行,即一行一行执行,直到遇到阻塞操作或者遇到异步事件为止。
  2. 函数调用:同步函数调用会阻塞代码执行,直到函数执行完成并返回结果。

异步事件

任务不直接进入主线程,而是由事件触发线程调度,在满足特定条件时先将异步任务放入事件处理队列(任务队列或者微任务队列)中,等待主线程空闲时处理。

  1. 定时器:通过 setTimeout()setInterval() 等函数创建的定时器是异步事件,它们会在指定的时间间隔之后触发回调函数。并且注意一点,当设置定时器时,浏览器会将定时器任务放入任务队列中。在 JavaScript 中,任务队列中的任务会按照顺序执行,并且会等待当前执行栈中的任务执行完成后才执行。因此,定时器回调函数的实际触发时刻可能会受到其他任务执行时间的影响。
  2. 事件监听器:通过 addEventListener() 等函数注册的事件监听器是异步事件,它们会在事件发生时触发回调函数。
  3. Promise:Promise 是一种用于处理异步操作的方式,它可以表示一个异步操作的结果,并在操作完成或失败时触发相应的回调函数。
  4. 异步函数:通过 asyncawait 关键字定义的异步函数是异步事件,它们可以让代码在等待异步操作完成时暂停执行,并在操作完成后继续执行。
  5. Fetch API:Fetch API 是用于发送 HTTP 请求的接口,它返回的 Promise 对象是异步事件,可以在请求完成时触发相应的回调函数。
  6. MutationObserver:当 DOM 变动符合观察条件时,MutationObserver 会将相应的变动添加到任务队列中,在合适的时机(通常是当前 JavaScript 执行栈执行完毕后)异步执行其回调函数。

Promise 和 MutationObserver 属于微任务。关于宏任务、微任务的定义将在后文进一步解释。

总的来说,同步事件是指会阻塞代码执行的事件,而异步事件是指不会阻塞代码执行的事件,可以在后台进行处理,并在需要时触发相应的回调函数。

2.5 事件循环(Event Loop)机制

1. 机制

回到开篇的问题:那如果所有的同步任务和异步任务,最终都会等待 JavaScript 主线程去处理,如何让这些任务有序地被处理呢?

答案是通过事件循环(Event Loop)机制来确保所有任务的有序执行,而不会阻塞主线程的执行。

事件循环是一个持续运行的过程,它不断地从消息队列中取出待执行的回调函数,并将其压入调用栈中执行。当调用栈为空时,事件循环会等待新的任务加入调用栈,或者从消息队列中取出新的任务执行。

每个代理(前面解释过)都是由事件循环驱动,事件循环的基本工作原理和执行顺序如下:

  1. 事件循环负责收集事件(比如用户交互、网络请求完成等)和将任务排队,以便在适当的时候执行它们。这些任务包括宏任务微任务
  2. 在事件循环的每一次迭代中,首先执行处于等待中的所有宏任务。宏任务通常包括整个 script 脚本、setTimeout 回调函数、UI 渲染等。
  3. 在执行完所有的宏任务之后,事件循环会执行微任务队列中的所有微任务。微任务通常包括 Promise 回调MutationObserver 回调等。
  4. 在处理完所有的任务之后,事件循环可能会执行一些必要的渲染和绘制操作,以确保页面的及时更新。
  5. 一旦所有任务都被执行完毕,并且可能的渲染和绘制操作也完成了,事件循环会开始下一个循环,重复上述过程。

这个过程确保了在 JavaScript 运行环境中任务的有序执行,并且及时地处理了各种类型的任务,从而保证了用户体验的流畅性和页面的及时更新。

2. (宏)任务和微任务队列

如下面一段代码:

setTimeout(function(){},0);
Promise.resolve(3).then((res)=> console.log(res));

执行顺序:先执行后者,为什么?

在 JavaScript 中,

setTimeout

Promise

都是异步任务的代表。虽然它们都被称为异步任务,但是它们的执行时机和机制略有不同。

  1. setTimeout:
setTimeout

会将指定的函数放入宏任务队列中,在指定的延迟时间后执行。即使设置了延迟时间为 0,

setTimeout

中的回调函数仍然会被放入宏任务队列,等待下一个事件循环中执行。

  1. Promise.resolve(3).then:
Promise.resolve(3)

会立即返回一个已解决(resolved)状态的 Promise 对象。

then()

方法注册的回调函数会被放入微任务队列中,在当前宏任务执行完成后立即执行微任务队列中的任务。

由于微任务队列的执行优先级高于宏任务队列,因此

then()

回调函数会先于

setTimeout

的回调函数执行。

注意,这里是说微任务队列的执行优先级高于宏任务队列,而不是微任务优先级高于宏任务。所以

setTimeout

还是比

promise

先执行,只是它的回调函数被放入了宏任务队列,要在下一次事件循环中才执行。

(宏)任务队列和微任务队列的区别很简单:

  • (宏)任务队列:当执行来自任务队列的任务时,在每一次新的事件循环开始迭代的时候,运行时都会执行队列中的每个任务,而在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行,如 setTimeout
  • 微任务队列:每次当一个任务退出且执行上下文为空时,微任务队列中的每一个微任务会一次性被执行,不同的是它会等到微任务队列为空才会停止执行,即假如中途有微任务加入,会接着执行这些新的微任务。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行前且当前事件循环结束之前执行完所有的微任务。所以有时候面试会考查为什么执行很多 promise 后才执行 settimeout 的回调。

请添加图片描述

3. tick

事件循环中,每一次循环或迭代,称为 tick,每一次 tick 的任务如下:

  1. 选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束,如果遇到后加入到宏任务,需要等待下一轮 tick 开始之后才执行
  2. 当当前 tick 下宏任务执行结束,检查是否存在微任务,有则会执行至微任务队列为空。注意即使中间有新的微任务加入,也会在本轮 tick 执行完
  3. 如果宿主为浏览器,可能会渲染页面
  4. 开始下一轮 tick,执行上一轮 tick 宏任务队列中的遗留宏任务(如中途加入的 setTimeout 的回调)

三、补充:Web 浏览器三类“事件循环”

3.1 window 事件循环

每个窗口(或标签页)都有自己的事件循环,称为 window 事件循环。window 事件循环驱动所有同源的窗口。

这里的网络术语 “window” 实际上指的用于运行网页内容的浏览器级容器,包括实际的 window,一个 tab 标签或者一个 frame。

在特定情况下,同源窗口之间共享事件循环,例如:

  1. 如果一个窗口打开了另一个窗口,它们可能会共享一个事件循环
  2. 如果窗口是包含在 <iframe> 中,则它可能会和包含它的窗口共享一个事件循环
  3. 在多进程浏览器中多个窗口碰巧共享了同一个进程

这种特定情况依赖于浏览器的具体实现,各个浏览器可能并不一样。

3.2 Worker 事件循环

Worker 是在后台运行的 JavaScript 程序,也有自己的事件循环,称为 Worker 事件循环。

worker 事件循环驱动 worker,包括 web worker、shared worker、service worker。

worker 被放在一个或多个独立于“主代码”的代理中,浏览器可能会用单个或多个事件循环来处理给定类型的所有 worker。

3.3 Worklet 事件循环

Worklet 是用于 Web 动画、音频、字体等场景的一种机制,它也有自己的事件循环,称为 Worklet 事件循环。

worklet 事件循环驱动 worklet,包括 workerlet、AudioWorklet、PaintWorklet。

什么是 worklet?

Web 动画和效果通常由 JavaScript 控制,但是当 JavaScript 执行繁重的计算时,可能会导致动画卡顿或页面响应变慢。为了解决这个问题,浏览器引入了 Worklet 技术。Worklet 是一种在浏览器中运行的脚本类型,与 Web Worker 类似,但它专门用于处理动画、渲染和音频等任务。Worklet 允许开发者在单独的线程中执行这些任务,从而避免阻塞主线程,提高了页面的性能和响应速度。

Worklet 主要有两种类型:

  • Paint Worklet(绘制 Worklet):用于自定义 CSS Paint API,可以动态地绘制 DOM 元素的背景、边框等样式。
  • Audio Worklet(音频 Worklet):用于自定义 Web Audio API,可以实现高性能的音频处理和合成。

Worklet 与主线程之间的通信是通过消息传递机制实现的,类似于 Web Worker。通过 Worklet 技术,开发者可以更好地优化页面的性能,并实现更复杂、更流畅的 Web 动画和交互效果。

四、参考阅读

  • 深入:微任务与 Javascript 运行时环境
标签: 前端 javascript

本文转载自: https://blog.csdn.net/weixin_58540586/article/details/137879890
版权归原作者 @PHARAOH 所有, 如有侵权,请联系我们删除。

“WHAT - 前端事件循环(Event Loop)机制”的评论:

还没有评论