mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
feat: 浏览器渲染原理
This commit is contained in:
@@ -1,2 +1,235 @@
|
||||
# 浏览器布局与DOM绘制
|
||||
# 事件循环
|
||||
|
||||
|
||||
## 浏览器多进程架构
|
||||
从安全性、高性能等原因出发,目前浏览器已经是多进程架构模式,至于演进历史,本文不再展开,感兴趣的可以查看这篇[“Electron” 一个可圈可点的 PC 多端融合方案]()文章。
|
||||
|
||||
现在的架构设计如下:
|
||||

|
||||
|
||||
一个页面最少包括:1个网络进程、1个浏览器进程、1个 GPU 进程、多个渲染进程、多个插件进程
|
||||
|
||||
- 浏览器进程:负责界面展示、用户交互(比如滚动条)、子进程管理协作、同时提供存储功能。同时内部会启动多个线程处理不同的任务
|
||||
- 渲染进程:核心任务是将 HTML、CSS、Javascript 转换为用户可以与之的网页,排版引擎 Blink 和 Javascript 引擎 V8 都是运行在该进程中。默认情况下,Chrome 为每个 tab 标签页创建一个新的渲染进程。出于安全考虑,渲染进程都是运行在沙箱机制下的。渲染进程启动后,会开启一个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。
|
||||
- GPU 进程:最早 Chrome 刚发布的时候是没有 GPU 进程的,而 GPU 的使用初衷是实现 css 3D 效果。随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍需求。最后 Chrome 多进程架构中也引入了 GPU 进程。
|
||||
- 网络进程:主要负责页面的网络资源请求加载。早期是作为一个模块运行在浏览器进程里面的,最近才独立出来作为一个单独的进程。网络进程内部会启动多个线程来处理不同的网路任务
|
||||
- 插件进程:主要负责插件的运行。因插件代码由普通开发者书写,所以在 QA 方面可能不是那么完善,代码质量参差不齐,插件容易奔溃,所以需要通过插件进程来隔离,以保证插件进程的奔溃不会对浏览器和页面造成影响。
|
||||
|
||||
所以,你会发现打开一个页面,查看进程发现有4个进程。凡事具有两面性,上面说了多进程架构带来浏览器稳定性、安全性、流畅性,但是也带来一些问题:
|
||||
|
||||
- 更高资源占用:每个进程都会包含公共基础结构的副本(如 Javascript 运行环境),这意味着浏览器将会消耗更多的资源
|
||||
- 更复杂的体系结构:浏览器各模块之间耦合度高、拓展性差,会导致现在的架构很难适应新需求。
|
||||
|
||||
|
||||
|
||||
## 渲染主线程是如何工作的
|
||||
|
||||
渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:
|
||||
|
||||
- HTML 解析和构建 DOM 树:渲染主线程会解析 HTML 代码,并构建 DOM(文档对象模型)树。DOM 树是网页的结构化表示,它描述了网页中的元素、标签和它们之间的关系。
|
||||
- CSS 解析和构建样式树:渲染主线程会解析 CSS 样式表,并构建样式树。样式树表示了网页中的元素和它们的样式信息,包括颜色、字体、布局等属性。
|
||||
- 布局(Layout):渲染主线程会根据 DOM 树和样式树计算元素在页面中的位置和大小,确定它们的布局。布局过程也被称为回流(reflow)或重排(relayout)。
|
||||
- 绘制(Painting):渲染主线程会将布局后的元素绘制到屏幕上,形成可见的网页内容。这个过程包括生成绘制命令、将命令发送给图形系统,并最终在屏幕上绘制出来。
|
||||
- JavaScript 执行:渲染主线程会执行网页中的 JavaScript 代码,处理交互、动画和事件等。JavaScript 的执行可能会引起 DOM 树和样式树的变化,进而触发布局和绘制。
|
||||
- 处理用户输入:渲染主线程会监听用户的输入事件,如鼠标点击、键盘输入等,并根据用户的操作进行相应的处理。这包括响应用户的点击、滚动、拖拽等操作,以及处理表单提交和页面跳转等事件。
|
||||
|
||||
渲染主线程是渲染进程中的核心部分,它负责将网页的结构、样式和交互转化为可视化的网页内容。通过优化渲染主线程的性能和效率,可以提高网页的渲染速度和用户体验。同时,渲染主线程也需要与其他线程(如网络线程、合成线程等)进行协作,以实现流畅的页面渲染和交互效果。
|
||||
|
||||
|
||||
|
||||
## 主线程如何有条不紊工作
|
||||
|
||||
浏览器主线程很繁忙,遇到以下情况该怎么处理:
|
||||
|
||||
- 正在执行某个方法,此时用户点击了按钮触发了事件,该立即去执行点击事件的处理函数吗?
|
||||
- 正在执行某个方法,此时某个计时器到达了时间,该立即去执行它的回调吗?
|
||||
- Input 输入框内容改变了,触发了 `oninput` 事件 ,此时刚好某个计时器也到达了时间,回调函数和 oninput 事件如何调度?
|
||||
|
||||
熟悉其他系统设计的同学可能会立马想到用**队列**来解决问题。浏览器对这个 case 也采用队列,叫做事件队列。
|
||||
|
||||
<img src="./../assets/JSMainThreadEventLoop.png" style="zoom:30%;">
|
||||
|
||||
|
||||
|
||||
解释下这个事件队列:
|
||||
|
||||
1. 一开始,渲染主线程开启一个事件循环(也可以认为是死循环一直在跑)
|
||||
2. 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态
|
||||
3. 其他所有线程(包括浏览器其他进程中的线程)都可以向这个事件队列中添加任务,新任务会被添加到队列尾部。添加新任务时,如果主线程是休眠的,则会唤醒主线程,继续从事件队列头部读取任务并不断执行
|
||||
4. 持续这个流程
|
||||
|
||||
当然,这个解释是相对宏观宽泛的,具包括哪些队列、不同任务里的任务优先级是怎么样的下面会逐步展开。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1. 执行当前的同步任务:JS 引擎每次执行当前的同步任务,将其推入调用栈中执行。同步任务是按照代码顺序执行的,知道遇到异步任务
|
||||
2. 执行微任务队列:在执行同步任务的过程中,如果遇到微任务(Microtask)如 Promise 的回调函数、MutationObserver 的回调函数则被添加到微任务队列(Microtask Queue)中。当前的同步任务执行完毕后,在进入下一轮事件循环之前,JS 引擎会按照顺序执行微任务队列中的事件。
|
||||
3.
|
||||
|
||||
1. 每次循环都会检查消息队列中是否存在任务,如果存在,就取出第一个任务执行,执行完一个后进入另一个
|
||||
|
||||
|
||||
|
||||
## 异步任务
|
||||
|
||||
### 何为异步
|
||||
|
||||
代码在执行的过程中会遇到一些无法立即处理的逻辑,比如:
|
||||
|
||||
- 计时器完成之后需要执行的任务:setTimeout、setInterval
|
||||
- 网络通信完成后需要执行的任务:XHR、Fetch
|
||||
- 用户交互后需要执行的任务:addEventListener
|
||||
|
||||
如果渲染主线程遇到这些任务时等待的话,就会让主线程处于等待的状态,对于用户而言,就是浏览器的「卡死」。
|
||||
|
||||
应对这种情况,浏览器采用异步的设计来解决,如下图
|
||||
|
||||
<img src="./../assets/JSEventloop.png" style="zoom:40%;">
|
||||
|
||||
使用异步的方案,可以使得主线程不等待、不阻塞,高效有序的执行逻辑。
|
||||
|
||||
何为异步?异步(Asynchronous)是指一种非阻塞的执行方式,允许同时执行多个任务而不会阻塞主线程。异步任务是指那些不会立即执行或不会阻塞代码执行的任务。
|
||||
|
||||
以下是常见的异步操作和写法:
|
||||
|
||||
1. 回调函数:使用回调函数是一种常见的处理异步操作的方式。在执行异步操作时,可以提供一个回调函数,在操作完成后调用回调函数来处理结果。
|
||||
2. Promise:ES6 引入的一种处理异步操作的机制。它可以用于处理异步操作的成功和失败,并支持链式调用。
|
||||
3. async/await:ES8 引入的一种更直观处理异步操作的语法。通过在函数前面加上 `async` 关键字,可以在函数内部使用 `await` 关键字来等待异步操作的结果。
|
||||
4. 定时器函数:JavaScript 提供了一些定时器函数,如 `setTimeout` 和 `setInterval`,它们可以用来延迟执行代码或定期执行代码
|
||||
5. XHR 和 Fetch:通过使用 AJAX 或 Fetch API,可以发送异步请求并获取服务器返回的数据。
|
||||
6. 交互事件:当事件发生时,注册好的回调函数通过异步来处理事件,而不会阻塞主线程的执行
|
||||
|
||||
|
||||
|
||||
### 异步的意义
|
||||
|
||||
浏览器是一个多进程架构,包含诸多进程,不同进程核心处理的问题不一样。浏览器进程负责界面展示、用户交互(比如滚动条)等。JS 是一门单线程语言,因为运行在浏览器渲染主线程中。然而这个浏览器主线程工作特别繁忙,渲染页面、执行 JS、负责用户交互等,如果采用同步的方式,很有可能会导致主线程阻塞,从而导致消息队列 中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白 白的消耗时间,另一方面导致⻚面无法及时更新,给用户造成卡死现象。
|
||||
|
||||
为了解决同步带来的问题,浏览器采用异步的设计。做法是渲染主线程存在一个事件队列,当某些任务(上述描述的异步任务)发生时,主线程会将该任务派发给其他线程处理(比如定时任务交给计时器线程处理),自身立即结束该任务的执行,转而执行后续逻辑。当派发给其他线程的异步任务执行完毕后,该异步事件的回调函数将被包装成任务,添加到消息队列的尾部,待主线程调度执行。
|
||||
|
||||
这种设计使得即使是单线程的 JS,运行也不会卡顿,渲染主线程有条不紊、高效的执行,保证了浏览器的流畅性。
|
||||
|
||||
**通过使用异步,可以更好地处理耗时的操作、网络请求、文件读写等任务,提高程序的性能和用户体验。**
|
||||
|
||||
|
||||
|
||||
## JS 为何会阻塞渲染?
|
||||
|
||||
由于 JS 可以操作 DOM,如果在修改 DOM 元素属性的同时渲染界面(JS 线程与 UI 线程同时运行),那么可能会发生一些不符合预期的结果了,渲染线程前后获得的元素数据不一致。
|
||||
|
||||
因此为了防止产生渲染产生不符合预期的结果,**浏览器设置 GUI 渲染线程和 JS 引擎为互斥关系**。当 JS 引擎执行时 GUI 线程会被挂起。GUI 的更新会被保存在一个队列中,等到 JS 引擎线程空闲时立即被执行。
|
||||
|
||||
因为互斥,所以可以推导出,JS 如果执行时间过长就会阻塞页面。
|
||||
|
||||
举个例子:
|
||||
|
||||
```
|
||||
<h1>你好</h1>
|
||||
<button>问好</button>
|
||||
<script>
|
||||
let h1 = document.querySelector('h1');
|
||||
let btn = document.querySelector('button');
|
||||
let delay = function(duration) {
|
||||
let startTime = Date.now();
|
||||
while (Date.now() - startTime < duration) {}
|
||||
}
|
||||
btn.onclick = function () {
|
||||
h1.textContent = '早上好,小刘';
|
||||
delay(3000);
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
效果就是:点击按钮修改 h1 标签内的文本。但是在点击后停了3秒后才开始执行。那这个现象如何解释呢?
|
||||
|
||||
T0 时刻:最开始的时候,主线程没有任务任务需要执行。但是主线程告诉交互线程,你需要监听用户的点击事件,点击后需要执行 callback
|
||||
|
||||
<img src="./../assets/JS-UIClickLag1.png" style="zoom:60%;">
|
||||
|
||||
T1时刻:当用户点击后,交互线程会把 callback 封装成一个任务,添加到事件循环队列的尾部。此时主线程依旧没有任务,所以主线程事件循环将被唤醒,从事件队列的头部取出一个任务去执行。大的一个任务里包含2个子事件:修改 DOM 和 延迟3秒。修改 DOM 这句指令执行后,浏览器要想看的见,需要内部会产生一个绘制任务(硬件设备显示图形的画家算法)。绘制任务被添加到事件队列尾部后,立马执行延迟3秒的事件。
|
||||
|
||||
<img src="./../assets/JS-UIClickLag2.png" style="zoom:60%;">
|
||||
|
||||
T2时刻:到达T2时刻后,主线程又空了,此时从事件队列中读取绘制任务。进而去显示出 DOM 文本修改后的结果(但此时前面已经等待了3秒钟,所以体感上会有一种卡顿的现象)
|
||||
|
||||
<img src="./../assets/JS-UIClickLag3.png" style="zoom:60%;">
|
||||
|
||||
|
||||
|
||||
## 任务有优先级吗
|
||||
|
||||
任务没有优先级,因为在消息队列中,无差别从对头选出任务,给队尾添加任务。
|
||||
|
||||
**但消息队列是有优先级的。**怎么理解?因为存在多个队列
|
||||
|
||||
根据 W3C 定义:
|
||||
|
||||
- 每个任务都有一个任务类型,同一种任务类型的任务归属于同一个队列。不同任务类型的任务被派发到不同的队列中(在一次事件循环中,浏览器可以根据实际情况从不同的任务队列中取出任务并执行)
|
||||
- 浏览器必须事先准备好1个微任务队列。微队列中的任务优先所有其他任务执行,优先级最高
|
||||
|
||||
浏览器的复杂度急剧提升,W3C 不再使用宏队列的说法。
|
||||
|
||||
目前 chromium 中,至少包含以下队列:
|
||||
|
||||
- 延时队列:用于存放计时器到达后的回调任务,优先级「中」
|
||||
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
|
||||
- 微队列:用户存放需要最快执行的任务,优先级「最高」
|
||||
|
||||
添加任务到微队列的主要方式主要是使用 Promise、MutationObserver。此外新加入的 [`queueMicrotask()`](https://developer.mozilla.org/zh-CN/docs/Web/API/queueMicrotask) 方法增加了一种标准的方式,可以安全的引入微任务。
|
||||
|
||||
|
||||
|
||||
## JS 事件循环
|
||||
|
||||
事件循环又叫消息循环,是指在单线程中处理任务队列的机制。
|
||||
|
||||
1. JS 引擎会创建一个全局作用域,并将其添加到调用栈中
|
||||
2. 如果有同步任务,则直接进入主线程执行该任务
|
||||
3. 若存在微任务或者用户交互事件、定时器事件等异步任务,则分门别类,添加到对应的任务队列中
|
||||
4. 当所有的同步任务执行完毕后,JS 引擎开始按照队列优先级检查任务队列是否存在未执行的任务
|
||||
- 如果微任务队列中存在任务,则优先从微任务队列对头取出1个任务执行,然后再次返回到主线程检查是否有新的同步任务。重复此过程,直到微任务队列为空
|
||||
- 如果微任务队列已经没有任务了,则从交互队列队列中判断,如果用户触发了某个事件,则把交互队列中对应的回调取出来放到主线程执行
|
||||
- 如果交互队列为空了,而定时器队列不为空,则判断定时器任务有没有到时见,如果已经达到可执行的条件,则把对应的回调事件放到主线程进行执行
|
||||
- 如果微任务队列和用户交互队列、定时器队列都为空,那么 JS 引擎会抛出一个“There are no more tasks to run.“ 的错误信息,表示事件循环结束
|
||||
|
||||
|
||||
|
||||
QA:JS 计时器准吗?
|
||||
|
||||
不准。存在以下几个原因:
|
||||
|
||||
- 计算机硬件没有原子钟,无法做到精确计时。依靠网络来和拥有原子钟的服务器进行同步
|
||||
- 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,也就携带了这些偏差
|
||||
- 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层,则会带有 4 毫秒的最少时间
|
||||
- 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -29,11 +29,11 @@ GUI 架构:过程化绘制(drawLine、drawRect)-> 面向对象抽象时代
|
||||
|
||||
3天时间写了个 PC 端应用程序。先看看结果吧
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ npm install && npm start
|
||||
```
|
||||
|
||||
简单介绍下 Demo 工程,工程目录如下所示
|
||||

|
||||

|
||||
|
||||
在终端执行 `npm start` 执行的是 package.json 中的 `scripts` 节点下的 start 命令,也就是 `Electron .`,`.` 代表执行 main.js 中的逻辑。
|
||||
|
||||
@@ -159,7 +159,7 @@ Electron 分为**渲染进程和主进程**。和 Native 中的概念不一样
|
||||
|
||||
单进程浏览器指的是浏览器的所有功能模块都是运行在同一个进程里的,这些模块包括网络、插件、Javascript 运行环境、渲染引擎和页面等。如此复杂的功能都在一个进程内运行,所以导致浏览器出现不稳定、不安全、不流畅等问题。
|
||||
|
||||

|
||||

|
||||
|
||||
早在2007年之前,市面上的浏览器都是单进程架构。
|
||||
|
||||
@@ -194,7 +194,7 @@ Electron 分为**渲染进程和主进程**。和 Native 中的概念不一样
|
||||
|
||||
#### 1.2 早期多进程架构浏览器
|
||||
|
||||

|
||||

|
||||
|
||||
上图2008年 Chrome 发布时的进程架构图。可以看出 Chrome 的页面是运行在单独的渲染进程中,同时页面的插件也是运行在单独的插件进行中的,进程之间通过 IPC 进行通信。
|
||||
|
||||
@@ -218,7 +218,7 @@ Electron 分为**渲染进程和主进程**。和 Native 中的概念不一样
|
||||
|
||||
Chrome 团队不断发展,目前架构有了较新变化,最新 Chrome 架构图如下所示
|
||||
|
||||

|
||||

|
||||
|
||||
最新 Chrome 浏览器包括:1个网络进程、1个浏览器进程、1个 GPU 进程、多个渲染进程、多个插件进程。
|
||||
|
||||
@@ -247,7 +247,7 @@ Chrome 团队一直在寻求新的弹性方案,既可以解决资源占用较
|
||||
|
||||
Chrome 最终把 UI、数据库、文件、设备、网络等模块重构为基础服务。下图是 “Chrome 面向服务的架构”的进程模型图
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -257,7 +257,7 @@ Chrome 最终把 UI、数据库、文件、设备、网络等模块重构为基
|
||||
|
||||
Chrome 提供灵活的弹性架构,在强大性能设备上会以多进程的方式运行基础服务,但是在设备资源受限的情况下,Chrome 会将很多服务整合到一个进程中,从而节省内存占用。
|
||||
|
||||

|
||||

|
||||
|
||||
#### 1.5 小实验
|
||||
|
||||
@@ -273,7 +273,7 @@ Chrome 提供灵活的弹性架构,在强大性能设备上会以多进程的
|
||||
|
||||
实验现象:
|
||||
|
||||

|
||||

|
||||
|
||||
实验结论:
|
||||
|
||||
@@ -310,7 +310,7 @@ Chrome 的默认策略是,每个标签对应一个渲染进程。但是如果
|
||||
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -331,7 +331,7 @@ Chrome 的默认策略是,每个标签对应一个渲染进程。但是如果
|
||||
|
||||
### 2. Electron 架构
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -353,7 +353,7 @@ Electron 架构和 Chromium 架构类似,也是具有1个主进程和多个渲
|
||||
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
上图描述了 Node.js 如何融入到 Chromium 中。描述下原理
|
||||
|
||||
@@ -380,7 +380,7 @@ Electron 架构和 Chromium 架构类似,也是具有1个主进程和多个渲
|
||||
|
||||
工程采用 Electron + Vue 技术,下面截图 Vue-devtools 很方便查看 Vue 组件层级等 Vue 相关的调试
|
||||
|
||||

|
||||

|
||||
|
||||
### 2. 主进程调试方式
|
||||
|
||||
@@ -395,10 +395,10 @@ Electron 架构和 Chromium 架构类似,也是具有1个主进程和多个渲
|
||||
```
|
||||
- 然后打开浏览器,在地址栏输入 `chrome://inspect`
|
||||
- 点击 `configure`,在弹出的面板中填写需要调试的端口信息
|
||||
- 
|
||||
- 
|
||||
- 重新开启服务 `npm start`,在 chrome inspect 面板的 `Target` 节点中选择需要调试的页面
|
||||
- 在面板中可以看到主进程执行的 `main.js`。可以加断点进行调试
|
||||

|
||||

|
||||
|
||||
方法二:利用 VS Code 调试 Electron 主进程。
|
||||
|
||||
@@ -428,7 +428,7 @@ Electron 架构和 Chromium 架构类似,也是具有1个主进程和多个渲
|
||||
|
||||
- 在调试模点击绿色小三角,会运行程序,可以添加断点信息。整体界面如下所示。可以单步调试、可以暂停、鼠标移上去可以看到对象的各种信息。
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -438,7 +438,7 @@ Electron 的渲染进程中的代码改变了,使用 Command + R 可以刷新
|
||||
|
||||
Webpack 有一个 api: `watch-run`,可以针对代码文件检测,有变化则 Restart
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -527,7 +527,7 @@ Electron 官方给出了解决方案 Squirrel,基于 Squirrel 框架完成的
|
||||
|
||||
9. Electron 多窗口与单窗口应用区别
|
||||
|
||||

|
||||

|
||||
|
||||
10. 知道 Electron 开发原理,所以大部分时间是在写前端代码。所以根据团队技术沉淀、选择对应的前端框架,比如 Vue、React、Angular。
|
||||
|
||||
@@ -535,7 +535,7 @@ Electron 官方给出了解决方案 Squirrel,基于 Squirrel 框架完成的
|
||||
|
||||
12. Electron 和 Web 开发相比,各自有侧重点
|
||||
|
||||

|
||||

|
||||
|
||||
13. 有些人开发 Electron 应用可能不喜欢 [electron-vue](https://github.com/SimulatedGREG/electron-vue) 这样的工具,喜欢自己自定义。假如自己利用 Vue 或者 React 开发的,开发过网页的同学都会习惯使用 Vue-devtools、React-devtools。所以在选用 Vue 或 React 后,习惯使用强大的 Vue-devtools、React-devtools 来查看 State、Action、Redux、Vuex、组件层级树等。
|
||||
|
||||
@@ -591,13 +591,13 @@ node --cpu-prof --heap-prof -e "require('request’)”“
|
||||
|
||||
### 1. 构建
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
### 2. 工程解耦
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -605,7 +605,7 @@ node --cpu-prof --heap-prof -e "require('request’)”“
|
||||
|
||||
Electron 提供的 crash 信息进行包装。
|
||||
|
||||

|
||||

|
||||
|
||||
```js
|
||||
import { BrowserWindow, app, dialog} from 'Electron';
|
||||
|
||||
53
Chapter2 - Web FrontEnd/2.44.md
Normal file
53
Chapter2 - Web FrontEnd/2.44.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Vue 核心原理探究
|
||||
|
||||
## 一、动手做一个超简易 Vue
|
||||
|
||||
先简单分析下 Vue 做了什么,巴拉巴拉,好多功能。就说一个最基础的,数据改变了页面就改变。如何实现?
|
||||
|
||||
1. `Object.defineProperty` 提供的 getter、setter 便可以拦截到所有 getter 的使用方,做到依赖收集
|
||||
2. 依赖收集:getter 方法内部可以知道谁在调用,此时可以做依赖收集。因为可能某个使用的方法(getFullName)会被调用多次,所以需要用 set 去收集,保持唯一。且给 window 对象上挂载一个内部属性的方式,来记录谁去调用过 getter。收集后立马清空 window 对象的私有属性值
|
||||
3. 派发执行:当数据改变的时候,也就触发到 setter,此时 setter 内部将该属性收集到的依赖方全部调用一遍方法。
|
||||
|
||||
```js
|
||||
|
||||
/**
|
||||
* 观察数据对象,数据驱动
|
||||
*
|
||||
* @param {any} obj
|
||||
*/
|
||||
function observe(obj) {
|
||||
for (const key in obj) {
|
||||
let getFunctionCallers = new Set();
|
||||
let internalValue = obj[key];
|
||||
Object.defineProperty(obj, key, {
|
||||
set: function (newValue) {
|
||||
internalValue = newValue;
|
||||
// set 完立即调用所有依赖 getter 的方法
|
||||
for (const getCaller of getFunctionCallers) {
|
||||
getCaller();
|
||||
}
|
||||
},
|
||||
get: function () {
|
||||
// 依赖收集
|
||||
if (window.__internalGetFunction) {
|
||||
getFunctionCallers.add(window.__internalGetFunction);
|
||||
}
|
||||
return internalValue;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用于依赖收集,将原始方法绑定到 window 的私有属性上
|
||||
*
|
||||
* @param {any} originalFunction
|
||||
*/
|
||||
function getFunctionProxy (originalFunction) {
|
||||
window.__internalGetFunction = originalFunction;
|
||||
originalFunction();
|
||||
window.__internalGetFunction = null;
|
||||
}
|
||||
```
|
||||
|
||||
498
Chapter2 - Web FrontEnd/2.45.md
Normal file
498
Chapter2 - Web FrontEnd/2.45.md
Normal file
@@ -0,0 +1,498 @@
|
||||
# 浏览器渲染原理
|
||||
|
||||
## 浏览器是如何渲染页面的
|
||||
|
||||
当浏览器的网络线程收到 HTML 文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。在事件循环机制下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/BrowserNetworkAndRender.png" style="zoom:20%;">
|
||||
|
||||
整个渲染流程分为多个阶段:HTML 解析、样式计算、布局、分层、绘制、分块、光栅化、画。每个阶段都有明确的输入输出,上一个阶段的输出就是下一个阶段的输入,整个流程类似流水线一样。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/BrowserRenderFullProgress.png" style="zoom:20%;">
|
||||
|
||||
下面针对每个阶段做详细的研究。
|
||||
|
||||
|
||||
|
||||
## 一、解析 HTML
|
||||
|
||||
解析的过程中遇到 CSS 便解析 CSS,遇到 JS 执行 JS。为了提高解析效率,浏览器会在开始解析器安,启动一个预解析线程,率先下载 HTML 中和外部 CSS、外部 JS 文件。
|
||||
|
||||
如果主线程解析到 link 标签,此时外部的 CSS 文件还没下载解析好,主线程不会等待,会继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 CSS 不会阻塞 HTML 解析的核心原因。
|
||||
|
||||
如果主线程解析到 script 位置,会停止解析 HTML,转而等待 JS 文件下载好,直到脚本加载和解析完成后,才能继续解析 HTML。这是因为 JS 代码执行过程可能会修改当前的 DOM,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。
|
||||
|
||||
第一步完成后,会得到 DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部样式、内联样式均会包含在 CSSOM 树中
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/HTMLParse-CSSOMDOM.png" style="zoom:20%;">
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/HTMLParse-DOM.png" style="zoom:20%;">
|
||||
|
||||
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/HTMLParse-CSSOM.png" style="zoom:20%;">
|
||||
|
||||
QA:
|
||||
|
||||
1. 为什么需要解析成 DOM、CSSOM?HTML 本身是字符串,无法操作,抽象成一层 JS 文档对象模型,并暴露一些 API 给 开发者,方便去操作。
|
||||
2. 为什么 HTML、CSS 都有自己对应的 DOM、CSSOM,但 JS 没有自己对应的模型?因为 CSS、HTML 是需要经常变化的,后续的逻辑会经常操作,所以有自己对应的树形模型,JS 执行一次就结束了。
|
||||
|
||||
|
||||
|
||||
### HTML 解析过程中遇到 CSS 怎么处理
|
||||
|
||||
为了提高解析效率,浏览器会启动一个预解析器率先下载和解析 CSS
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/HTMLParse-CSSPreload.png" style="zoom:20%;">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 包含哪些样式
|
||||
|
||||
HTML 解析会生成 DOM、CSSOM。StyleSheetList 是所有样式的集合,那包含哪些样式呢?
|
||||
|
||||
- 浏览器默认样式(比如 div 独自占一行)
|
||||
- 内联样式
|
||||
- 内部样式
|
||||
- 外部样式
|
||||
|
||||
浏览器默认样式怎么体现。翻阅 Chromium 的源代码可以查看到浏览器为 html 设置的默认样式,针对 div 标签设置了 `display: block` 所以才独占一行,而不是因为是 div 所以就该占一行。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Chromium-BrowserDefaultStyle.png" style="zoom:30%;">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### HTML 解析过程中遇到 JS 代码怎么办
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/HTMLParse-JSDownload.png" style="zoom:30%;">
|
||||
|
||||
|
||||
|
||||
浏览器渲染主线程遇到默认的 script 标签(不带 async、defer 修饰)则会暂停解析 HTML,等待 JS 下载并执行完毕后方可继续解析 HTML。
|
||||
|
||||
预解析线程可以分担下载 JS 的任务。
|
||||
|
||||
QA:为什么 JS 不能设计成像 css 那样一边解析,一边执行呢?
|
||||
|
||||
JS 可能会操作当前 DOM,DOM 不是全部的 HTML 解析完才创建的,解析多少 HTML 创建多少 DOM,所以需要暂停 HTML 解析(类似生产者消费者模型,不加锁则可能存在问题。HTML 解析就是在生产 DOM,JS 脚本可能一边在读取 DOM,可能还在增、删 DOM 在消费 DOM)
|
||||
|
||||
|
||||
|
||||
### 为什么浏览器遇到 script 会暂停解析?
|
||||
|
||||
浏览器在解析 HTML 的时候,如果遇到一个没有任何属性的 script 标签,就会暂停 HTML 解析,先发送网络请求获取该 JS 脚本的内容,然后让 JS 引擎执行该代码。 当 JS 执行完毕后恢复 HTML 的解析,整个过程如下
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/HTMLParse-JSDefaultBehavior.png" style="zoom:50%;">
|
||||
|
||||
解决方式:`defer ` 和 `async` 都是异步加载的外部 JS 脚本,不会阻塞页面的解析,因此这2个方案都可以规避因为大量 JS 下载影响 HTML 解析的问题
|
||||
|
||||
#### async
|
||||
|
||||
async 表示异步,当浏览器遇到带有 async 的 script 时,浏览器会采用异步的方式去解析,不会阻塞浏览器解析 HTML,一旦网络请求完成后,如果此时 HTML 还没解析完的话,立即暂停 HTML 的解析工作,让 JS 引擎执行 JS 脚本,全部 JS 执行完毕后再去解析后面的 HTML。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/HTMLParse-JSAsync1.png" style="zoom:50%;">
|
||||
|
||||
如果遇到 async 修饰的 js 脚本,在异步下载好之后 HTML 已经解析完毕,也一样,直接执行 JS
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/HTMLParse-JSAsync2.png" style="zoom:50%;">
|
||||
|
||||
|
||||
|
||||
#### defer
|
||||
|
||||
defer 也表示异步延迟,当浏览器遇到带有 defer 的 script 时,会异步获取该脚本,不会阻塞浏览器解析 HTML,一旦网络请求回来后,如果 HTML 解析还未完成,则会继续解析 HTML,直到 HTML 全部解析完成后才执行 JS 代码。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/HTMLParse-JSDefer.png" style="zoom:50%;">
|
||||
|
||||
如果存在多个 defer 标签修饰的 script 标签,浏览器会按照 script 顺序执行,不会破坏脚本之间的依赖关系
|
||||
|
||||
|
||||
|
||||
## 二、样式计算
|
||||
|
||||
主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出最终的样式,称为 Computed Style。
|
||||
|
||||
在这一过程中,很多预设值会变成绝对值,比如 red 会变为 rgb(255, 0, 0),相对单位也会变成绝对单位,比如 em 变成 px。这一步完成后会得到一颗带有样式的 DOM 树。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StyleCompute.png" style="zoom:30%;">
|
||||
|
||||
什么叫计算后的样式(Computed Style):指的是元素在渲染时所采用的最终样式。
|
||||
|
||||
样式计算主要包括:CSS 属性值的计算过程(层叠、继承...)、视觉格式化模型(盒模型、包含块、BFC...)
|
||||
|
||||
这个计算涉及到:
|
||||
|
||||
- 确定声明值
|
||||
- 层叠冲突
|
||||
- 使用继承
|
||||
- 使用默认值
|
||||
|
||||
### CSS 属性计算过程
|
||||
|
||||
```css
|
||||
<div>
|
||||
<p>p</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
没有修改p 标签的样式,却可以看到他有一个颜色、字体等样式。
|
||||
|
||||
该元素上会有 css 的所有属性。在 Chrome 审查元素模式下,勾选 Computed 下的 Show all,就可以看到非常多的属性如下图:
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CSSStyleComputeDemo.png" style="zoom:20%;">
|
||||
|
||||
也就是说:我们开发的任何一个 HTML 元素,实际上在浏览器显示出来的时候都有一套完整的 css 样式。如果没有显示声明样式,大概率会采用默认值。
|
||||
|
||||
|
||||
|
||||
#### 确定声明值
|
||||
|
||||
```html
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
<div>
|
||||
<h1>H1标题</h1>
|
||||
<p>段落</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
css 代码明确了 p 标签使用红色,那么浏览器展示就会按照此属性进行展示。
|
||||
|
||||
这种开发者写的代码,叫做作者样式表。一般浏览器还会存在用户代理样式表,可以认为是浏览器内置了一份样式表。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ComputedStyleDemo-p.png" style="zoom:30%;">
|
||||
|
||||
可以看到 p 标签在审查元素模式下,除了作者样式表中设置的 color 属性,其他的都采用了用户代理样式表的属性。比如:display...
|
||||
|
||||
|
||||
|
||||
#### 层叠冲突
|
||||
|
||||
前面看了确定声明值,但可能存在一种情况,声明的样式规则发生冲突。
|
||||
|
||||
此时会进入**解决层叠冲突**的流程,分为3个步骤:
|
||||
|
||||
1. 比较源的重要性
|
||||
2. 比如优先级
|
||||
3. 比较次序
|
||||
|
||||
|
||||
|
||||
#### 比较源的重要性
|
||||
|
||||
当不同的 css 样式来源拥有相同的声明时,此时就会根据样式表来源的重要性来确定该用哪一条样式规则。那,有几种样式表来源:
|
||||
|
||||
- 浏览器会有一个基础的样式表来给任何网页设置默认的样式。该样式被称为:用户代理样式
|
||||
- 网页的作者可以定义文档的样式,这是最常见的样式,被称为:页面作者样式
|
||||
- 浏览器的用户,可以使用自定义样式表定制使用体验。被称为用户样式
|
||||
|
||||
重要性的顺序为:页面作者样式 > 用户样式 > 用户代理样式
|
||||
|
||||
|
||||
|
||||
假设现在有页面作者样式表和用户代理样式表中存在属性冲突,那会以页面作者样式表优先。
|
||||
|
||||
```html
|
||||
p {
|
||||
color: red;
|
||||
display: inline-block;
|
||||
}
|
||||
<div>
|
||||
<h1>H1标题</h1>
|
||||
<p>段落</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CssStylePriority.png" style="zoom:25%;">
|
||||
|
||||
可以看到 p 标签的 display 属性,在页面作者样式表和用户代理样式表中同时存在时,根据源的重要性判断,最终页面作者样式表优先级更高,display 采用页面作者样式表中的属性值。
|
||||
|
||||
|
||||
|
||||
#### 比较优先级
|
||||
|
||||
如果同一个源中,样式声明一样的情况下,如何决策?此时进入了样式声明的优先级比较。
|
||||
|
||||
```html
|
||||
.container p {
|
||||
color: #00ff00;
|
||||
}
|
||||
p {
|
||||
color: red;
|
||||
display: inline-block;
|
||||
}
|
||||
<div class="container">
|
||||
<h1>H1标题</h1>
|
||||
<p>段落</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
可以看到,在上面的 css 代码中,都在页面作者样式表同一个源中,源一样,解下去根据选择器的权重来比较重要性。
|
||||
|
||||
根据选择器权重规则(ID 选择器 > 类选择器 > 类型选择器)来判断,单独一个标签选择器的 color 属性会被打败。
|
||||
|
||||
关于选择器的比较策略可以查看 [MDN CSS Specificity](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Specificity)
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CssStyleDeclarationPriority.png" style="zoom:25%;">
|
||||
|
||||
|
||||
|
||||
#### 比较次序
|
||||
|
||||
当样式表同源,权重相同的情况下,进入第三阶段:比较声明的顺序
|
||||
|
||||
```html
|
||||
.container p {
|
||||
color: #00ff00;
|
||||
}
|
||||
.container p {
|
||||
color: #0000ff;
|
||||
}
|
||||
<div class="container">
|
||||
<h1>H1标题</h1>
|
||||
<p>段落</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
都处于页面作者样式表中,选择器的权重也相同,但根据所处位置的不同,下面的样式声明会覆盖上面的值,最终采用 #0000ff。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CssStylePositionPriority.png" style="zoom:25%;">
|
||||
|
||||
样式声明冲突的情况解决了
|
||||
|
||||
|
||||
|
||||
#### 使用继承
|
||||
|
||||
层叠冲突这一步完成后,解决了相同元素被声明的多条样式规则命中后,到底该采用哪一条样式规则的问题。
|
||||
|
||||
那假设一个元素没有声明的属性,该使用什么属性值?会存在继承这个策略。
|
||||
|
||||
```html
|
||||
.container {
|
||||
color: #00ff00;
|
||||
}
|
||||
<div class="container">
|
||||
<h1>H1标题</h1>
|
||||
</div>
|
||||
```
|
||||
|
||||
只对类名为 container color 进行了设置,针对 p 标签没有任何的设置,但由于 color 可以继承,所以 p 就从最近的 div 继承了颜色。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CssStyleInherited.png" style="zoom:25%;">
|
||||
|
||||
看另一个现象
|
||||
|
||||
```html
|
||||
.container {
|
||||
color: blue;
|
||||
}
|
||||
.innerContainer {
|
||||
color: red;
|
||||
}
|
||||
<div class="container">
|
||||
<div class="innerContainer">
|
||||
<h1>H1标题</h1>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CssStyleDoubleInherited.png" style="zoom:25%;">
|
||||
|
||||
这里继承了 container、innerContainer 2个的属性值,说了继承会选择更近的一个,innerContainer 胜出。
|
||||
|
||||
|
||||
|
||||
#### 使用默认值
|
||||
|
||||
如果前面的步骤都走了,但属性值还没确定下来,就只能选用默认值了。
|
||||
|
||||
```html
|
||||
<div>
|
||||
<h1>H1标题</h1>
|
||||
</div>
|
||||
```
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CssStyleDefaultValue.png" style="zoom:25%;">
|
||||
|
||||
任何一个元素要在浏览器上渲染出来,必须具备所有的 css 属性值,但很多属性我们没有去设置,用户代理样式表中也没有设置,也无法从继承中拿到,因此最终都是使用默认值的。
|
||||
|
||||
|
||||
|
||||
## 三、布局 - layout
|
||||
|
||||
布局完成后会得到布局树。
|
||||
|
||||
布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息,比如节点的宽高、相对包含块的位置。
|
||||
|
||||
元素的尺寸和位置,会受它的包含块所影响。对于一些属性,例如 width, height, padding, margin,绝对定位元素的偏移值(比如 position 被设置为 absolute 或 fixed),当我们对其赋予百分比值时,这些值的计算值,就是通过元素的包含块计算得来。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Layout.png" style="zoom:25%;">
|
||||
|
||||
大部分时候,DOM 树和 Layout 树并非一一对应的,为什么?
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CSSLayoutTreeDisplay.png" style="zoom:25%;">
|
||||
|
||||
比如某个节点的 display 设置为 none,这样的节点就没有几何信息,因此不会生成到 layout 树。有些使用了伪元素选择器,虽然 DOM 树中并不存在这些伪元素节点,但它们需要显示在浏览器上,所以会生成到 layout 树上。还有匿名行盒、匿名块盒等等都会导致 DOM 树和 layout 树无法一一对应。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CSSLayoutTreePseudoClass.png" style="zoom:25%;">
|
||||
|
||||
```html
|
||||
div::before {
|
||||
content: '';
|
||||
}
|
||||
<div></div>
|
||||
```
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CSSPseudoClassLayout.png" style="zoom:25%;">
|
||||
|
||||
## 四、分层 Layer
|
||||
|
||||
浏览器拿到布局后的 layout tree 会思考一些事情。大多数页面不是绘制后静止的,经常会有一些动画、用户点击后一些交互处理等,需要经常刷新页面。但如果整个页面直接重新刷新,这个性能开销是很大的。能不能提高效率?能,于是现在浏览器支持了分层。
|
||||
|
||||
主线程会使用一套复杂的策略对整个布局树进行分层。
|
||||
|
||||
分层的好处在于,将来在某一个层改变后,仅会对该层进行后续处理,从而提高效率。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Layer.png" style="zoom:25%;">
|
||||
|
||||
滚动条、堆叠上下文有关的(z-index、transform、opacity )样式都会或多或少影响分层结果,也可以通过 will-change 属性更大程度的影响分层的结果。
|
||||
|
||||
```html
|
||||
<div>
|
||||
100个<p>Lorem</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Layer-Demo.png" style="zoom:25%;">
|
||||
|
||||
可以看到默认情况下浏览器对该代码分了2层。为什么滚动条需要单独设置一个 layer?因为100个 p 标签一定会超出一屏,所以会存在滚动条,且用户可能会高频滚动去查看内容,为了高效渲染,所以将滚动条单独设置了一个层。
|
||||
|
||||
假设我们已经通过技术手段得知某些内容经常会改变,但直接页面整体重刷会很耗费资源,那如何只对某一块区域设置一个 layer 呢?通过 `will-change: transform` 告诉浏览器,这块区域的 transform 属性经常会变。
|
||||
|
||||
`will-change`是一个用于通知浏览器某个元素即将发生变化的CSS属性。它可以被应用到任何元素上,用于提前告知浏览器该元素将要有哪些属性进行改变,从而优化渲染性能。
|
||||
|
||||
通过在元素上设置`will-change`属性,开发者可以明确指示浏览器对该元素进行优化处理。这样一来,浏览器可以提前分配资源和准备工作,以便在实际改变发生之前进行相应的合成操作。这样做有助于避免不必要的重绘和重排,提高页面的响应速度和动画的流畅度。
|
||||
|
||||
`will-change`属性可以接受多个属性值,表示将要改变的属性。例如,`will-change: transform`表示元素即将进行变形操作,`will-change: opacity`表示元素的透明度即将发生变化。
|
||||
|
||||
需要注意的是,`will-change`属性应在实际变化发生前的一段时间内被设置,以便浏览器有足够的时间进行准备和优化。另外,`will-change`属性并不会自动触发硬件加速,但它可以为浏览器提供一种优化渲染的提示。
|
||||
|
||||
此外,`will-change`属性的使用应谨慎,避免滥用。**只有在明确知道元素即将发生某种变化,并且这种变化对性能有影响时,才应使用`will-change`属性。过度使用`will-change`属性可能会导致浏览器进行不必要的优化,反而降低性能**。
|
||||
|
||||
总的来说,`will-change`属性是一个用于优化渲染性能的CSS属性,通过提前告知浏览器元素即将发生的变化,使浏览器能够提前进行准备和优化,从而提高页面的响应速度和动画的流畅度
|
||||
|
||||
Demo:
|
||||
|
||||
```
|
||||
div {
|
||||
will-change: transform;
|
||||
width: 200px;
|
||||
background-color: red;
|
||||
margin: 0 auto;
|
||||
}
|
||||
```
|
||||
|
||||
通过上面的 css 设置,告诉浏览器:这个 div 的 transform 经常会变,浏览器会为该元素创建一个独立的图层,将这个图层标记为“即将变换”。这样,在进行布局和绘制时,浏览器就可以更高效地处理这个元素,而无需重新计算整个渲染树。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Layer-Demo2.png" style="zoom:25%;">
|
||||
|
||||
|
||||
|
||||
## 五、绘制 - Paint
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Paint.png" style="zoom:25%;">
|
||||
|
||||
第四步的产出就是分层。第五步绘制阶段,会分别对每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来(类似 canvas 代码,告诉计算机从哪个点经过中间几个点,最后到哪个点绘制一条线,中间内容用什么颜色填充)
|
||||
|
||||
渲染主线程的工作到此为止,剩余步骤交给其他线程完成。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-MainThread-duty.png" style="zoom:25%;">
|
||||
|
||||
## 六、分块 - Tiling
|
||||
|
||||
完成绘制之后,主线程将每个图层的绘制信息提交给合成线程,剩余的工作将由合成线程完成。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Tilling.png" style="zoom:25%;">
|
||||
|
||||
合成线程首先对每个图层分块(Tiling),将其划分为更多的小区域。合成线程类似一个任务调度者,它会从线程池中拿出多个线程来完成分块的工作。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Tilling-Worker.png" style="zoom:25%;">
|
||||
|
||||
QA:浏览器渲染过程中分块的作用是什么?
|
||||
|
||||
1. 提高渲染性能:当浏览器需要渲染一个复杂的网页时,如果一次性将所有内容加载并渲染出来,可能会消耗大量的计算资源和时间,通过网页分成多个较小的块(tiling)浏览器可以并行地加载和渲染这些块,从而充分利用计算资源,提高渲染性能。
|
||||
2. 优化内存占用:如果浏览器一次性加载和渲染整个网页,可能会消耗大量的内存。通过将网页分成多个图块,浏览器可以更好的管理内存,避免不必要的内存消耗
|
||||
3. 实现懒加载:通过分块,浏览器可以实现懒加载,即只有当某个图块进入视口可见区域时,才开始加载和渲染该图块。这样可以减少不必要的加载和渲染,提高页面的加载速度和性能
|
||||
4. 方便进行并行处理和异步操作:通过将网页分成多个图块,浏览器可以更容易地进行并行处理和异步操作。例如,在移动端设备上,可以利用 GPU 进行图块的并行渲染,提高渲染效率。
|
||||
|
||||
分块 tiling 技术是浏览器渲染过程中提高性能、优化内存使用和实现懒加载的重要手段之一。
|
||||
|
||||
|
||||
|
||||
## 七、光栅化
|
||||
|
||||
分块完成后会进入光栅化阶段。光栅化是将每个块变成位图。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Raster.png" style="zoom:25%;">
|
||||
|
||||
|
||||
|
||||
合成线程将分块后的块信息交给 GPU 进程,以极高的速度完成光栅化,并优先处理靠近视口的块。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Raster-GPU.png" style="zoom:25%;">
|
||||
|
||||
|
||||
|
||||
## 八、画 - Draw
|
||||
|
||||
合成线程计算出每个位图在屏幕上的位置,交给 GPU 进行最终的呈现。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Draw.png" style="zoom:25%;">
|
||||
|
||||
合成线程拿到每个层(Layer)、每个块(Tile)的位图(bitmap)后,生成一个个指引(quad)信息。指引信息标识出每个位图应该画到屏幕上的哪个位置,此过程会考虑到旋转、缩放、变形等。
|
||||
|
||||
因为变形发生在合成线程,与渲染主线程无关,这也是 transform 效率高的本质原因。
|
||||
|
||||
合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕显示
|
||||
|
||||
|
||||
|
||||
完成过程如下
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Full-Progress.png" style="zoom:50%;">
|
||||
|
||||
|
||||
|
||||
## reflow
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Reflow.png" style="zoom:40%;">
|
||||
|
||||
比如某个业务逻辑,导致我们用 js 脚本修改了某个节点的宽度、颜色,这其实修改的是 cssom,假设业务逻辑导致操作了 DOM,DOM 节点增删了,cssom、DOM 改变了,则会触发一系列的的流程,比如重新计算样式 style、布局(layout)、分层 layer、绘制 paint、分块 tiling、光栅化 raster、画 draw、系统调用 GPU 去真正显示。
|
||||
|
||||
当渲染树 render tree 中的一部分(或者全部)因为元素的尺寸、布局、颜色、隐藏等改变而需要重新构建,这就情况被称为回流。图上的 style 这部分。
|
||||
|
||||
回流发生时,浏览器会让渲染树中受到影响的部分失效,并重新构建这部分渲染树。完成回流 reflow 后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。
|
||||
|
||||
**简单来说,reflow 就是计算元素在屏幕上确切的位置和大小并重新绘制。回流的代价远大于重绘。回流必定重绘,重绘不一定回流。**
|
||||
|
||||
|
||||
|
||||
## repaint
|
||||
|
||||
当渲染树 render tree 中的一些元素需要更新样式,但这些样式只是改变元素外观、风格,而不影响布局(位置、大小),则叫重绘。
|
||||
|
||||
**简单来说,重绘就是将渲染树节点转换为屏幕上的实际像素,不涉及重新布局阶段的位置与大小计算**
|
||||
|
||||
|
||||
|
||||
QA:为什么 transform 效率高?
|
||||
|
||||
假设在 css 中针对某个元素写了 `transform: rotate(100deg)`,transform 既不会影响布局也不会影响绘制指令,它影响的是渲染流程的最后一个阶段,图上的 draw 阶段。由于 draw 阶段发生在合成线程中,不在渲染主线程,所以 transform 的任何变化几乎不会影响主线程。同样,不管主线程如何繁忙,都不影响合成线程中 transform 的变化。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Browser-Reflow.png" style="zoom:40%;">
|
||||
@@ -32,7 +32,7 @@
|
||||
* [28、神器Puppeteer](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.28.md)
|
||||
* [29、从Vue.js谈谈前端开发的技术栈演变](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.29.md)
|
||||
* [30、Javascript 常用工具封装](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.30.md)
|
||||
* [31、浏览器布局与DOM绘制](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.31.md)
|
||||
* [31、事件循环](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.31.md)
|
||||
* [32、React核心技术剖析](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.32.md)
|
||||
* [33、ES6学习总结](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.33.md)
|
||||
* [34、富文本编辑器的原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.34.md)
|
||||
@@ -43,4 +43,6 @@
|
||||
* [39、前端模块化演进之路](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.39.md)
|
||||
* [40、“Electron” 一个可圈可点的 PC 多端融合方案](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.40.md)
|
||||
* [41、sourceMap 闪亮登场](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.41.md)
|
||||
* [42、JS原型链与Objective-C内存布局不能说的秘密](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.43.md)
|
||||
* [42、JS原型链与Objective-C内存布局不能说的秘密](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.43.md)
|
||||
* [43、Vue 核心原理探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.44.md)
|
||||
* [44、浏览器渲染原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.45.md)
|
||||
Reference in New Issue
Block a user