浏览器端的EventLoop

本文深入探讨了浏览器中的EventLoop机制,详细阐述了浏览器渲染进程中的JS引擎线程、GUI渲染线程、事件触发线程、定时触发器线程和异步请求线程的工作原理。解释了同步任务与异步任务的执行流程,以及宏任务和微任务在事件循环中的角色。同时,通过实例分析了代码执行顺序,帮助理解JS的执行模型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

EventLoop就是事件循环,在前端概念中通常用来指浏览器或者Node环境中,用于解决JavaScript单线程运行时不会阻塞的一种机制。这篇文章我们主要来了解一下浏览器中JS的EventLoop是怎么运行的。
在进入EventLoop的概念前,我们不妨先大致梳理一下两个概念,即:

浏览器在这中间到底做了什么事情 以及 JS是单线程运行的。

进程/线程

身为web前端工程师,我们使用的JS多运行在浏览器上。每当我们打开一个浏览器的时候,实际上我们就是在我们新打开了一个浏览器进程,在浏览器进程中我们才运行的JS线程

进程就是一个工厂 --->  工厂间也是互相独立的
线程就是每个工厂的工人  ---> 工人 相互协作在工厂中完成任务

在这里插入图片描述

在我们任务管理器中打开,第一个tab就是进程列表
实际上进程就是cpu资源分配的一个单位
线程就是cpu能调度的一个单位
一般通用的叫法:单线程与多线程,都是指在一个进程内的单线程和多线程

同时我们的浏览器也是多进程的,比如我开了17个谷歌的tab页。浏览器帮我+1了
简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。
(如果浏览器是单进程,那么某个Tab页崩溃了,直接整个浏览器全G了)

所以我们得出了个结论:在浏览器中打开一个网页相当于新起了一个进程

浏览器渲染进程

JS执行,页面渲染,事件循环都是在我们浏览器渲染的这个进程中执行0的。

1.GUI渲染进程
负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等
当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),
GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
2.JS引擎线程
也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
JS引擎线程负责解析Javascript脚本,运行代码。
JS引擎一直等待着任务队列中任务的到来,然后加以处理。
一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序。
同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,就会导致页面渲染加载阻塞。
3.事件触发线程
归属于浏览器而不是JS引擎,用来控制事件循环(JS引擎自己都忙不过来,需要浏览器另开线程协助)
当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
4.定时触发器线程
setInterval与setTimeout所在线程
浏览器定时计数器并不是由JavaScript引擎计数的
(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
因此通过单独线程来计时并触发定时
(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
5.异步请求线程
XHR在连接后是通过浏览器新开一个线程请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

在这里插入图片描述

小问题: css加载是否会阻塞dom树渲染?

css是由单独的下载线程异步下载的
css加载不会阻塞DOM树解析(异步加载时DOM照常构建)
但会阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息)

JS的EventLoop

到此时,已经是属于浏览器页面初次渲染完毕后的事情,JS引擎的一些运行机制分析。
这里主要是结合Event Loop来谈JS代码是如何执行的,不做多的引申。
在这里我们会用到上文的几个概念:

1.JS引擎线程
2.事件触发线程
3.定时触发器线程

JS分为同步任务和异步任务
同步任务都在主线程上执行,形成一个执行栈
主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

练习题

阅读以下代码写出打印的执行顺序

console.log(1);

setTimeout(() => {
  console.log(2);
}, 1000);

console.log(3);

setTimeout(() => {
  console.log(4);
}, 0);

console.log(5);
执行的顺序应该为
1.读取console.log(1) 同步执行 JS引擎线程执行
2.读取到setTimeout,添加到定时触发器线程中,定时器触发器开始计时,1s后推到任务队列里
3.读取到console.log(3),同步执行 JS引擎线程执行
4.读取到setTimeout,添加到定时触发器线程中,定时器触发器开始计时,4ms后推到任务队列里
5.读取到console.log(5),同步执行 JS引擎线程执行
6.读取到JS引擎线程执行完成,系统读取任务队列
所以最终的 打印顺序是15342

在JS的事件循环中,还需要知道宏任务与微任务的概念

宏任务:MacroTask
如:setTimeout、setInterval、script、 I/O 操作等。
微任务:MicroTask
如: Promise,Ajax,async等

宏任务队列可以有多个
微任务队列只能有一个

在这里插入图片描述

1. 任务会依次进入执行栈,而首先入场的就是——宏任务全局上下文(script);
2. 执行同步任务;
3. js 引擎遇到一个异步任务后并不会一直等待其返回结果:
	3.1 遇到异步任务,交给异步处理模块处理,对应的异步处理线程处理异步任务需要的操作,例如定时器的计数和异步请求监听状态的变更;
	3.2 当异步事件返回结果后,事件触发线程将回调函数(标记)加入任务队列,等待栈为空时,依次进入栈中执行;
4. 执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空:
	4.1 先检查微任务(microTask)队列,如果存在任务,则一次性执行完所有微任务,无任务则跳过
	4.2 后检查宏任务(macroTask)队列,如果存在任务,则取出第一个宏任务,执行,

又因为第一次进入执行栈的总是宏任务(Script),而每次宏任务完成后,都会读取微任务队列,所以,大家也会听见另一种表述方式:
每一轮循环中,同步任务执行后会在末尾执行并清空所有的微任务,会在下一轮循环中取第一个宏任务执行,如此循环

再看一下下面这道题
	console.log(1)
	new Promise(resolve => {
		console.log(2)
		resolve()
		console.log(3)
	}).then(() => {
		console.log(4)
	})
	console.log(5)
	setTimeout(function() {
		console.log(6)
	})
	//请打印结果
首先我们知道,promise构造函数是同步执行的,then方法本身是同步执行,then方法中的内容加入微任务异步执行
所以这道题的答案就是

再综合一下,我们看一道练习题

	var a
	var b
	console.log(a)
	async function chart1() {
		console.log(1)
		await chart2()
		console.log(2)
	}

	async function chart2() {
	    a = 2
	    console.log(a)
	    await console.log(4)
	    console.log(5)
	}
	
	setTimeout(function() {
	    console.log(6)
	    a = b?.a
	    console.log(a)
	})

	new Promise((resolve) => {
	    console.log(a)
		resolve({a : 1})
	}).then(val => {
	    b = val	
	    console.log(b)
	}).then(() => {
	    console.log(a+b)
	})

	chart1()
	// 请打印结果

在这里插入图片描述

内容概要:本文档详细介绍了在三台CentOS 7服务器(IP地址分别为192.168.0.157、192.168.0.158和192.168.0.159)上安装和配置Hadoop、Flink及其他大数据组件(如Hive、MySQL、Sqoop、Kafka、Zookeeper、HBase、Spark、Scala)的具体步骤。首先,文档说明了环境准备,包括配置主机名映射、SSH免密登录、JDK安装等。接着,详细描述了Hadoop集群的安装配置,包括SSH免密登录、JDK配置、Hadoop环境变量设置、HDFS和YARN配置文件修改、集群启动与测试。随后,依次介绍了MySQL、Hive、Sqoop、Kafka、Zookeeper、HBase、Spark、Scala和Flink的安装配置过程,包括解压、环境变量配置、配置文件修改、服务启动等关键步骤。最后,文档提供了每个组件的基本测试方法,确保安装成功。 适合人群:具备一定Linux基础和大数据组件基础知识的运维人员、大数据开发工程师以及系统管理员。 使用场景及目标:①为大数据平台建提供详细的安装指南,确保各组件能够顺利安装和配置;②帮助技术人员快速掌握Hadoop、Flink等大数据组件的安装与配置,提升工作效率;③适用于企业级大数据平台的建与维护,确保集群稳定运行。 其他说明:本文档不仅提供了详细的安装步骤,还涵盖了常见的配置项解释和故障排查建议。建议读者在安装过程中仔细阅读每一步骤,并根据实际情况调整配置参数。此外,文档中的命令和配置文件路径均为示例,实际操作时需根据具体环境进行适当修改。
在无线通信领域,天线阵列设计对于信号传播方向和覆盖范围的优化至关重要。本题要求设计一个广播电台的天线布局,形成特定的水平面波瓣图,即在东北方向实现最大辐射强度,在正东到正北的90°范围内辐射衰减最小且无零点;而在其余270°范围内允许出现零点,且正西和西南方向必须为零。为此,设计了一个由4个铅垂铁塔组成的阵列,各铁塔上的电流幅度相等,相位关系可自由调整,几何布置和间距不受限制。设计过程如下: 第一步:构建初级波瓣图 选取南北方向上的两个点源,间距为0.2λ(λ为电磁波波长),形成一个端射阵。通过调整相位差,使正南方向的辐射为零,计算得到初始相位差δ=252°。为了满足西南方向零辐射的要求,整体相位再偏移45°,得到初级波瓣图的表达式为E1=cos(36°cos(φ+45°)+126°)。 第二步:构建次级波瓣图 再选取一个点源位于正北方向,另一个点源位于西南方向,间距为0.4λ。调整相位差使西南方向的辐射为零,计算得到相位差δ=280°。同样整体偏移45°,得到次级波瓣图的表达式为E2=cos(72°cos(φ+45°)+140°)。 最终组合: 将初级波瓣图E1和次级波瓣图E2相乘,得到总阵的波瓣图E=E1×E2=cos(36°cos(φ+45°)+126°)×cos(72°cos(φ+45°)+140°)。通过编程实现计算并绘制波瓣图,可以看到三个阶段的波瓣图分别对应初级波瓣、次级波瓣和总波瓣,最终得到满足广播电台需求的总波瓣图。实验代码使用MATLAB编写,利用polar函数在极坐标下绘制波瓣图,并通过subplot分块显示不同阶段的波瓣图。这种设计方法体现了天线阵列设计的基本原理,即通过调整天线间的相对位置和相位关系,控制电磁波的辐射方向和强度,以满足特定的覆盖需求。这种设计在雷达、卫星通信和移动通信基站等无线通信系统中得到了广泛应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值