【整理+总结】JavaScript 高级(二)浏览器和 JavaScript 的运行原理、内存管理

本文详细介绍了浏览器从URL输入到页面显示的渲染流程,包括HTML解析、CSS解析、构建RenderTree、布局和绘制,以及回流和重绘的概念。同时,文章深入探讨了JavaScript的V8引擎工作原理,包括AST、Ignition和TurboFan的角色。此外,还讲解了JavaScript的内存管理和闭包,以及如何防止内存泄漏。最后,讨论了script元素的defer和async属性对页面加载的影响。

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

本笔记整理自codewhy老师的腾讯课程《深入理解JavaScript高级语法》~

🔥🔥🔥更多知识,欢迎访问我的个人博客:Nan-ying’s Blog

🔥前端三件套之 JavaScript 高级地址
(一)函数 this 指向https://blog.youkuaiyun.com/weixin_42771853/article/details/129171448
(三)函数、对象增强和继承https://blog.youkuaiyun.com/weixin_42771853/article/details/129171589
(四)E6 ~E13 新特性https://blog.youkuaiyun.com/weixin_42771853/article/details/129171646
(五)Promise、异步async await、防抖节流等https://blog.youkuaiyun.com/weixin_42771853/article/details/129171690
(六)网络请求和响应https://blog.youkuaiyun.com/weixin_42771853/article/details/129171717

一、浏览器的运行原理

一个网页URL从输入到浏览器中,到显示经历过怎么样的解析过程呢?

在这里插入图片描述

1. 浏览器渲染流程

浏览器的内核

我们经常说的浏览器内核指的是浏览器的排版引擎

  • 排版引擎(layout engine),也称为浏览器引擎(browser engine)、页面渲染引擎(rendering engine)或样版引擎。
  • 也就是一个网页下载下来后,就是由我们的渲染引擎来帮助我们解析的

在这里插入图片描述

1.1 HTML 解析过程

  • 因为默认情况下服务器会给浏览器返回index.html文件,所以解析HTML是所有步骤的开始
  • 解析HTML,会构建 DOM Tree
    在这里插入图片描述

1.2 CSS 解析过程

  • 在解析的过程中,如果遇到 CSS 的 link 元素,那么会由浏览器负责下载对应的 CSS 文件:
    • 注意:下载 CSS 文件是不会影响 DOM 的解析的
  • 浏览器下载完 CSS 文件后,就会对 CSS 文件进行解析,解析出对应的规则树
    • 我们可以称之为 CSSOM(CSS Object Model,CSS对象模型);
      在这里插入图片描述

1.3 构建 Render Tree

当有了 DOM Tree 和 CSSOM Tree 后,就可以两个结合来构建Render Tree

注意:

  • link 元素不会阻塞 DOM Tree 的构建过程,但是会阻塞 Render Tree 的构建过程
    • 这是因为 Render Tree 在构建时,需要对应的 CSSOM Tree;
  • Render Tree 和 DOM Tree 并不是一一对应的关系,比如对于 display = none 的元素,压根不会出现在 render tree 中;
    在这里插入图片描述

1.4 布局(layout)和绘制(paint)

  • 第四步是在渲染树(Render Tree)上运行布局以计算每个节点的几何体
    • 渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息;
    • 布局是确定呈现树中所有节点的宽度、高度和位置信息
  • 第五步是将每个节点绘制到屏幕上
    • 在绘制阶段,浏览器将布局阶段计算的每个frame转为屏幕上实际的像素点
    • 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如img)

2. 回流和重绘解析

回流(reflow):

  • 第一次确定节点的大小和位置,称之为布局(layout)。
  • 之后对节点的大小、位置修改重新计算称之为回流。
    ==什么情况下引起回流呢? ==
  • 比如DOM结构发生改变(添加新的节点或者移除节点);
  • 比如改变了布局(修改了width、height、padding、font-size等值)
  • 比如窗口resize(修改了窗口的尺寸等)
  • 比如调用getComputedStyle方法获取尺寸、位置信息;

重绘(repaint):

  • 第一次渲染内容称之为绘制(paint)。
  • 之后重新渲染称之为重绘。
    什么情况下会引起重绘呢?
  • 比如修改背景色、文字颜色、边框颜色、样式等;

回流一定会引起重绘, 很消耗性能,开发中要尽量避免发送回流:

  1. 修改样式时尽量一次性修改
    • 比如通过cssText修改,比如通过添加class修改
  2. 尽量避免频繁的操作DOM
    • 我们可以在一个 DocumentFragment 或者父元素中将要操作的DOM操作完成,再一次性的操作;
  3. 尽量避免通过getComputedStyle获取尺寸、位置等信 息;
  4. 对某些元素使用position=absolute或者fixed
    • 并不是不会引起回流,而是开销相对较小,不会对其他元素造成影响

3. 合成和性能优化

绘制的过程,可以将布局后的元素绘制到多个合成图层中。这是浏览器的一种优化手段

  • 默认情况下,标准流中的内容都是被绘制在同一个图层中
  • 某些特殊的CSS属性, 会生成新的合成图层,并且新的图层可以利用GPU来加速绘制(因为每个合成层都是单独渲染的)
  • 分层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用。

可以形成新的合成层的属性:

  • 3D transforms
  • video、canvas、iframe
  • opacity 动画转换时
  • position: fixed
  • will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化;
  • animation 或 transition 设置了opacity、transform;

4. script 元素和页面解析的关系

浏览器在解析 HTML 的过程中,遇到了 script 元素会停止继续构建 DOM 树,首先下载 JavaScript 代码,并且执行 JavaScript 的脚本; 只有等到 JavaScript 脚本执行结束后,才会继续解析 HTML,构建 DOM 树;

这是因为 JavaScript 的作用之一就是操作 DOM,并且可以修改 DOM;如果我们等到 DOM 树构建完成并且渲染再执行 JavaScript,会造成严重的回流和重绘,影响页面的性能

但是这个也往往会带来新的问题,在目前的开发模式中(比如Vue、React),脚本往往比HTML页面更“重”,处理时间需要更长;所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到

4.1 defer 属性

  • 告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree

  • 如果脚本提前下载好了,它会等待DOM Tree构建完成后DOMContentLoaded事件之前先执行defer中的代码

  • 多个带defer的脚本是可以保持正确的顺序执行的

  • 从性能的角度最好放到head中

  • defer对于非外部引用的script元素,无效

4.2 async 属性

  • async 属性也能让脚本不阻塞页面(与 defer 类似);
  • async 脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本;
  • async 不能保证在DOMContentLoaded之前或者之后执行
  1. defer 通常用于需要在文档解析后操作DOM的JavaScript代码,并且对多个script文件有顺序要求的;
  2. async 通常用于独立的脚本,对其他脚本,甚至DOM没有依赖的

5. 总结

浏览器输入一个URL到页面显示的过程:

  1. 首先通过DNS服务器进行域名解析,解析出对应的IP地址,然后从ip地址对应的主机发送http请求,获取对应的静态资源
  2. 默认情况服务器会返回index.html文件
  3. 然后浏览器内核开始解析HTML
    1. 首先会解析对应的html 生成DOM Tree
    2. 解析过程中,如果遇到css的link标签则会下载对应的css文件(下载css文件和生成DOM树同时进行)
    3. 下载完对应的css文件后会进行css解析 生成CSSOM( CSS object model)
    4. 当DOM Tree和CSSTree都解析完成之后 会进行合并用来生成Render Tree(渲染树)
    5. 初步生成的渲染树会显示节点以及部分样式,但是并不表示每个节点的尺寸、位置信息。于是进行布局来生成渲染树中节点的宽度、高度、位置信息
    6. 经过Layout之后,浏览器内核将布局时的每个frame转为屏幕上的每个像素点、将每个节点绘制到屏幕上

二、JavaScript 的运行原理

1. 深入 V8 引擎原理

浏览器内核是由两部分组成的,以webkit为例:

  • WebCore:负责HTML解析、布局、渲染等等相关的工作;
  • JavaScriptCore:解析、执行JavaScript代码;

另外一个强大的JavaScript引擎就是V8引擎

1.1 V8 引擎的架构

在这里插入图片描述

  • Parse 模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;
  • Ignition 是一个解释器,会将AST转换成ByteCode(字节码)
    • 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
    • 如果函数只调用一次,Ignition会解释执行ByteCode;
  • TurboFan 是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;
    • 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过 TurboFan 转换成优化的机器码,提高代码的执行性能;
    • 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码

在将JavaScript代码转换成AST(抽象语法树)的过程中:
在这里插入图片描述

词法分析:

  • 将对应的每一行的代码的字节流分解成有意义的代码块,代码块被称为词法单元
  • token是记号化(tokenization)的缩写
  • 词法分析器,也叫扫描器(scanner)

语法分析:

  • 将对应的tokens分析成一个元素逐级嵌套的树, 这个树称之为抽象语法树
  • 语法分析器也可以称之为 parser

在这里插入图片描述

2. 深入 JS 执行原理

2.1 执行全局代码

2.1.1 初始化全局对象

js引擎在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)

  • 该对象所有的作用域都可以访问;
  • 里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
  • 其中还有一个window属性指向自己

在这里插入图片描述

2.1.2 执行上下文

js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈

全局的代码块为了执行会构建一个 Global Execution Context(GEC); GEC会被放入到ECS中执行,其中内容包括:

  • ==第一部分:==在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到Global Object中,但是并不会赋值
    • 这个过程也称之为变量的作用域提升(hoisting)
  • ==第二部分:==在代码执行中,对变量赋值,或者执行其他的函数

2.1.3 认识VO对象

每一个执行上下文会关联一个VO(Variable Object,变量对象),变量和函数声明会被添加到这个VO对象中。

当全局代码被执行的时候,VO就是GO对象

全局代码执行过程(执行前)的内存示意图:

在这里插入图片描述

只有·会额外创建一个对象,objvar bar = function{}这些在此阶段都视作变量

全局代码执行过程(执行后)的内存示意图:

在这里插入图片描述

2.2 执行函数代码

在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC), 并且压入到EC Stack中。

因为每个执行上下文都会关联一个VO,那么函数执行上下文关联的VO是什么呢?

  • 当进入一个函数执行上下文时,会创建一个AO对象(Activation Object);
  • 这个AO对象会使用 arguments 作为初始化,并且初始值是传入的参数;
  • 这个AO对象会作为执行上下文的VO来存放变量的初始化

函数的执行过程(执行前)的内存示意图:

在这里插入图片描述

函数的执行过程(执行)的内存示意图:

在这里插入图片描述

函数代码多次调用
  1. foo第一次执行123:
    在这里插入图片描述

  2. foo第二次执行321:
    在这里插入图片描述

函数代码相互调用
  1. foo和bar函数执行:
    在这里插入图片描述

  2. bar函数执行完毕:
    在这里插入图片描述

  3. foo函数执行完毕:
    在这里插入图片描述

2.3 作用域、作用域链

当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)

  • 作用域链是一个对象列表,用于变量标识符的求值;
  • 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象;
函数变量的查找过程
  1. 函数有自己的message
    在这里插入图片描述

  2. 函数没有自己的message
    在这里插入图片描述

var message = "Global Message"
function foo() {
	console.log(message)
}
foo()

var obj = {
	name: "obj",
	bar: function () {
		var message = "bar message"
		foo()
	}
}

obj.bar() // Global Message 函数在刚创建时就已经形成了自己的作用域链,向上查找时为window
函数代码多层嵌套

在这里插入图片描述

面试题
var n = 100
function foo1() {
	console.log(n) //100 作用域链在定义时已经形成
}

function foo2() {
	var n = 200
	console.log(n) // 200
	foo1()
}
foo2()
var n = 100
function foo() {
	console.log(n) // undefined
	return
	var n = 200 // 已经定义
}
foo()

三、JS 的内存管理和闭包

1. 内存管理的理解

内存的管理都会有如下的生命周期:

  • 第一步:分配申请你需要的内存(申请);

  • 第二步:使用分配的内存(存放一些东西,比如对象等);

  • 第三步:不需要使用时,对其进行释放

  • JS对于原始数据类型内存的分配会在执行时, 直接在栈空间进行分配;

  • JS对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并且将这块空间的指针返回值 变量引用;

2. 垃圾回收机制GC算法

2.1 引用计数(了解)

  • 当一个对象有一个引用指向它时,那么这个对象的引用就+1;
  • 当一个对象的引用为0时,这个对象就可以被销毁掉;

这个算法有一个很大的弊端就是会产生循环引用

2.2 标记清除(可达性)

  • 标记清除的核心思路是可达性
  • 这个算法是设置一个根对象,垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于那些没有引用到的对象,就认为是不可用的对象
    在这里插入图片描述

2.3 其他算法补充

V8引擎为了进行更好的优化,它在算法的实现细节上也结合一些其他的算法:

  • 标记整理
    • 回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从而整合空闲空间,避免内存碎片化
  • 分带处理—— 对象被分成两组:“新的”和“旧的”
    • 许多对象出现,完成它们的工作并很快死去,它们可以很快被清理
    • 那些长期存活的对象会变得“老旧”,而且被检查的频次也会减少
  • 增量收集
    • 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟
    • 所以引擎试图将垃圾收集工作分成几部分逐一进行处理,这样会有许多微小的延迟而不是一个大的延迟
  • 闲时收集
    • 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响

3. 闭包的概念理解

一个函数和对其周围状态(词法环境)的引用捆绑在一起,这样的组合就是闭包(closure)

  • 一个普通的函数function,如果它可以访问外层作用域的自由变量,那么这个函数和周围环境就是一个闭包
    • 从广义的角度来说:JavaScript中的函数都是闭包
    • 从狭义的角度来说:JavaScript中一个函数,如果访问了外层作用域的变量,那么它是一个闭包

3.1 闭包的执行过程

  1. 第一次调用createAdder
    在这里插入图片描述

  2. 执行adder5
    在这里插入图片描述

  3. adder5执行完成
    在这里插入图片描述

  4. 第二次调用createAdder
    在这里插入图片描述

3.2 闭包的内存泄漏和释放

  • 在上面的案例中,如果后续我们不再使用adder8函数了,那么该函数对象应该要被销毁掉,并且其引用着的父作用域AO也应该被销毁掉;
  • 但是目前因为在全局作用域下adder8变量对0xb00的函数对象有引用,而0xb00的作用域中AO(0x200)有引用,所以最终会造成这些内存都是无法被释放的;
  • 所以我们经常说的闭包会造成内存泄露,其实就是刚才的引用链中的所有对象都是无法释放的;

此时,将 adder8 = null 即可释放内存
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值