Q: DOM树是怎么生成的?
从浏览器架构的层面来看,浏览器通常由以下几个主要组件构成:
-
用户界面(User Interface):用户界面是用户与浏览器进行交互的一部分,包括地址栏、菜单、工具栏等。它负责展示网页内容并接收用户的输入。
-
渲染引擎(Rendering Engine):渲染引擎负责解析 HTML、CSS 和 JavaScript,并将其转化为可视化的网页内容。渲染引擎将构建 DOM 树和 CSSOM 树,计算布局,最终将网页内容绘制在屏幕上。
-
JavaScript 引擎(JavaScript Engine):JavaScript 引擎负责解释和执行 JavaScript 代码。它将 JavaScript 代码转化为可执行的指令,并与渲染引擎进行交互,实现动态的网页交互和逻辑处理。常见的 JavaScript 引擎包括 V8(Chrome)、SpiderMonkey(Firefox)和JavaScriptCore(Safari)。
-
布局引擎(Layout Engine):布局引擎负责计算网页元素的布局和排列方式。它根据 DOM 树和 CSSOM 树的信息,确定每个元素的位置和尺寸,并处理元素之间的相互影响。
-
网络组件(Networking):网络组件负责处理网络请求和响应,包括发送 HTTP 请求、接收响应和处理数据传输。它还处理缓存、Cookie、安全等与网络相关的功能。
-
数据存储(Data Persistence):数据存储组件用于在浏览器中存储和管理数据,包括 Cookie、本地存储(Local Storage)、IndexedDB 等。
-
插件(Plugins):插件允许浏览器扩展其功能,例如 Flash 插件、PDF 阅读器等。然而,随着 Web 技术的发展,插件的使用逐渐减少,由原生 Web API 替代。
-
安全性组件(Security Components):安全性组件负责保护用户的隐私和安全,包括防止恶意代码执行、防止跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等。
以上是浏览器架构中的一些常见组件,不同浏览器可能有略微不同的实现细节和命名,但整体上它们共同协作,实现了浏览器对网页的解析、渲染、交互和网络访问等功能。
再具体一下DOM树如何被生成出来,*DOM(Document Object Model)树是由浏览器解析 HTML(或者 XML)文档时生成的。生成 DOM 树的过程包括以下步骤:
-
解析 HTML:浏览器首先将接收到的 HTML 文档进行解析,将其分解成一个个的标记(tokens)。这个过程称为词法分析(Lexical Analysis)和语法分析(Syntax Analysis)。
-
构建 DOM 节点:根据解析得到的标记,浏览器开始构建 DOM 树的节点。每个标记对应一个 DOM 节点,并按照其在文档中的层次关系来组织节点之间的父子关系。
-
树的生成:根据节点的父子关系,浏览器逐步将节点组织成树状结构。HTML 文档的根节点是
<html>,其下有<head>和<body>两个子节点,以此类推,直到构建完整的 DOM 树。 -
属性和文本内容的添加:浏览器继续解析标记中的属性和文本内容,并将其添加到相应的 DOM 节点上。标记中的属性会成为节点的属性,而文本内容则成为节点的文本子节点。
-
解析完成:当所有的标记都解析完毕并构建了相应的 DOM 节点后,DOM 树的生成过程就完成了。
DOM 树的生成是浏览器渲染网页的基础,它表示了文档的结构和内容,提供了对网页元素的访问和操作接口。通过访问 DOM 树,可以通过 JavaScript 来操作网页的结构和样式,实现动态的交互和内容更新。
Q: 上面说的「解析HTML」的过程中会有分词器进行语法分析和词法分析,那么当分析器遇到script标签时候会怎么办?
当遇到了scrpit会暂停html转换dom,去解析jascript,而async和defer会异步加载不会阻塞html转换”。
Q: 浏览器渲染进程下面有哪些线程,分别做什么用?
在浏览器渲染进程中,常见的线程包括:
-
主线程(Main Thread):也称为渲染线程或UI线程,主要负责处理用户交互、执行 JavaScript 代码、布局计算和页面渲染等任务。它负责将 HTML、CSS 和 JavaScript 解析成渲染树,并将其绘制到屏幕上。
-
样式计算线程(Style Calculation Thread):样式计算线程负责计算和处理元素的样式信息,例如解析 CSS 样式表、计算元素的样式值、处理样式优先级等。这些计算会生成 CSSOM(CSS Object Model)树,用于后续的布局和渲染操作。
-
布局线程(Layout Thread):布局线程负责计算页面元素的几何位置和尺寸,以确定它们在屏幕上的具体排列。它会根据 DOM 树和 CSSOM 树的信息,执行布局计算,并生成布局树(也称为盒模型树)。
-
绘制线程(Paint Thread):绘制线程负责将页面元素绘制到屏幕上。它根据布局树和样式信息,将页面内容转换为位图或矢量图,并将其渲染到屏幕上的对应位置。
-
合成线程(Compositing Thread):合成线程负责处理页面的图层合成。现代浏览器将页面的不同元素分割为多个图层,每个图层都有自己的绘制和合成过程。合成线程负责将这些图层合成为最终的页面图像,并在屏幕上显示。
除了以上线程,还有一些附属的线程用于处理特定的任务,例如事件线程(Event Thread)用于处理用户输入事件,网络线程(Network Thread)用于处理网络请求和响应,定时器线程(Timer Thread)用于管理定时器任务等。
这些线程在浏览器渲染进程中协同工作,执行不同的任务,从解析和计算到渲染和合成,以实现网页的快速加载和流畅的交互体验。线程之间的协调和通信通过线程间的消息传递和同步机制来实现。同时,为了提高性能和资源利用率,浏览器还会对线程进行优化,如使用线程池、异步任务调度等。
Q: V8引擎的垃圾回收机制是怎样的?
V8的内存结构
在V8引擎的堆结构组成中,其实除了新生代和老生代外,还包含其他几个部分,但是垃圾回收的过程主要出现在新生代和老生代,所以对于其他的部分我们没必要做太多的深入,有兴趣的小伙伴儿可以查阅下相关资料,V8的内存结构主要由以下几个部分组成:
新生代(new_space):大多数的对象开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。老生代(old_space):新生代中的对象在存活一段时间后就会被转移到老生代内存区,相对于新生代该内存区域的垃圾回收频率较低。老生代又分为老生代指针区和老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。大对象区(large_object_space):存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区。代码区(code_space):代码对象,会被分配在这里,唯一拥有执行权限的内存区域。map区(map_space):存放Cell和Map,每个区域都是存放相同大小的元素,结构简单(这里没有做具体深入的了解,有清楚的小伙伴儿还麻烦解释下)。
V8 是 Google Chrome 浏览器中使用的 JavaScript 引擎,它采用了自动垃圾回收机制来管理内存。V8 的垃圾回收机制主要包括以下几个组成部分:
-
分代式垃圾回收(
Generational Garbage Collection):V8 将内存分为不同的代(Generation),通常包括新生代(Young Generation)和老生代(Old Generation)。新生代用于存放生命周期较短的对象,老生代用于存放生命周期较长的对象。V8 使用不同的垃圾回收策略和算法来处理不同代的对象,以提高垃圾回收效率。 -
增量标记(
Incremental Marking):V8 的垃圾回收是基于标记-清除算法的,其中的一个关键步骤是标记阶段,即标记哪些对象是存活的。为了减少垃圾回收的停顿时间,V8 使用增量标记技术,将标记过程分成多个小步骤,与 JavaScript 执行交替进行,以使标记过程分散在多个时间片中,避免一次长时间的停顿。 -
Scavenge垃圾回收器:Scavenge垃圾回收器用于处理新生代中的对象。它将新生代内存空间分为两个相等的空间,分别称为 From 空间和 To 空间。当需要进行垃圾回收时,Scavenge垃圾回收器会先将 From 空间中的存活对象拷贝到 To 空间,然后清空 From 空间中的所有对象,完成垃圾回收。这种方式的好处是只需要复制存活对象,而不需要遍历整个堆,因此速度较快。 -
Mark-Sweep垃圾回收器:Mark-Sweep垃圾回收器用于处理老生代中的对象。它的工作过程分为标记阶段和清除阶段。在标记阶段,垃圾回收器通过根对象遍历整个对象图,并标记出存活的对象。在清除阶段,垃圾回收器会遍历堆中的所有对象,清除未标记的对象,回收其占用的内存空间。
除了以上主要的垃圾回收机制,V8 还包括一些其他的优化技术,如增量整理、惰性清除等,以提高垃圾回收的效率和性能。垃圾回收是 V8 引擎中非常重要的一部分,它帮助开发者自动管理内存,避免内存泄露。
Q:接上面的问题,为什么TO超过25%要晋升老生代?标记清除是怎么清除的?
对象晋升为了不影响后续FORM空间的分配,条件主要有以下两个:
- 对象是否经历过一次
Scavenge算法 To空间的内存占比是否已经超过25%
默认情况下,我们创建的对象都会分配在From空间中,当进行垃圾回收时,在将对象从From空间复制到To空间之前,会先检查该对象的内存地址来判断是否已经经历过一次Scavenge算法,如果地址已经发生变动则会将该对象转移到老生代中,不会再被复制到To空间,
标记清除的做法:垃圾回收会构建一个根列表,从根节点去访问那些变量,可访问到位活动,不可就是垃圾。
Q:如何避免内存泄漏?
尽可能少地创建全局变量
不要轻易在window上挂载全局变量
手动清除定时器
在我们的应用中经常会有使用setTimeout或者setInterval等定时器的场景,定时器本身是一个非常有用的功能,但是如果我们稍不注意,忘记在适当的时间手动清除定时器,那么很有可能就会导致内存泄漏,示例如下:
javascript
复制代码
const numbers = [];
const foo = function() {
for(let i = 0;i < 100000;i++) {
numbers.push(i);
}
};
window.setInterval(foo, 1000);
在这个示例中,由于我们没有手动清除定时器,导致回调任务会不断地执行下去,回调中所引用的numbers变量也不会被垃圾回收,最终导致numbers数组长度无限递增,从而引发内存泄漏。
少用闭包
闭包是JS中的一个高级特性,巧妙地利用闭包可以帮助我们实现很多高级功能。一般来说,我们在查找变量时,在本地作用域中查找不到就会沿着作用域链从内向外单向查找,但是闭包的特性可以让我们在外部作用域访问内部作用域中的变量,示例如下:
javascript
复制代码
function foo() {
let local = 123;
return function() {
return local;
}
}
const bar = foo();
console.log(bar()); // -> 123
在这个示例中,foo函数执行完毕后会返回一个匿名函数,该函数内部引用了foo函数中的局部变量local,并且通过变量bar来引用这个匿名的函数定义,通过这种闭包的方式我们就可以在foo函数的外部作用域中访问到它的局部变量local。一般情况下,当foo函数执行完毕后,它的作用域会被销毁,但是由于存在变量引用其返回的匿名函数,导致作用域无法得到释放,也就导致local变量无法回收,只有当我们取消掉对匿名函数的引用才会进入垃圾回收阶段。
清除DOM引用
以往我们在操作DOM元素时,为了避免多次获取DOM元素,我们会将DOM元素存储在一个数据字典中,示例如下:
javascript
复制代码
const elements = {
button: document.getElementById('button')
};
function removeButton() {
document.body.removeChild(document.getElementById('button'));
}
在这个示例中,我们想调用removeButton方法来清除button元素,但是由于在elements字典中存在对button元素的引用,所以即使我们通过removeChild方法移除了button元素,它其实还是依旧存储在内存中无法得到释放,只有我们手动清除对button元素的引用才会被垃圾回收。
弱引用
通过前几个示例我们会发现如果我们一旦疏忽,就会容易地引发内存泄漏的问题,为此,在ES6中为我们新增了两个有效的数据结构WeakMap和WeakSet,就是为了解决内存泄漏的问题而诞生的。其表示弱引用,它的键名所引用的对象均是弱引用,弱引用是指垃圾回收的过程中不会将键名对该对象的引用考虑进去,只要所引用的对象没有其他的引用了,垃圾回收机制就会释放该对象所占用的内存。这也就意味着我们不需要关心WeakMap中键名对其他对象的引用,也不需要手动地进行引用清除,