Skip to content

Commit f645329

Browse files
author
victorsun
committed
upd
1 parent d1fc427 commit f645329

File tree

11 files changed

+703
-0
lines changed

11 files changed

+703
-0
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# 事件循环
2+
3+
事件循环,在W3C中描述为 `Event Loop`,Chrome的实现源码中定义为 `Message Loop`,因此事件循环也称为消息循环,是**浏览器渲染主线程**的工作方式。
4+
5+
6+
7+
## 1. 浏览器的进程模型
8+
9+
浏览器是一个多进程多线程的应用程序,核心的进程是**浏览器进程****网络进程****渲染进程**
10+
11+
**浏览器进程**:负责浏览器软件的UI界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同任务。
12+
13+
**网络进程**:负责加载网络资源。
14+
15+
**渲染进程**:(重点) 渲染进程启动后,会开启一个**渲染主线程**,主线程负责执行 HTML、CSS、JS。默认情况下,浏览器会为每个标签页开启一个**新的渲染进程**,以保证标签页隔离 (未来可能改为一个站点一个进程)。
16+
17+
18+
19+
## 2. 渲染主线程的异步处理
20+
21+
渲染主线程是浏览器中最繁忙的线程,处理的任务包括但不限于:
22+
23+
+ 解析 HTML
24+
+ 解析 CSS
25+
+ 计算样式
26+
+ 布局
27+
+ 处理图层
28+
+ 帧绘制
29+
+ 执行全局 JS 代码
30+
+ 执行事件处理函数
31+
+ 执行计时器的回调函数
32+
+ ......
33+
34+
JS运行在浏览器的单个渲染进程的渲染主线程中,而渲染主线程只有一个!注意,js的单线程,是因为执行在浏览器的渲染主线程,并不代表浏览器是单进程/线程的。
35+
36+
因此,JS是一门 **单线程** 的语言,浏览器采用异步而非同步的方式来避免阻塞,如计时器、网络、事件监听。
37+
38+
主线程将任务交给其他线程处理,完成后将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。
39+
40+
**总结**
41+
42+
1. **单线程是异步产生的原因**
43+
44+
2. **事件循环是异步的实现方式**
45+
46+
47+
48+
**异步的场景**`setTimeout``setInterval``XHR``Fetch``addEventListener`
49+
50+
![](./img/01-event_loop.jpg)
51+
52+
53+
54+
## 3. JS 阻塞渲染
55+
56+
如下面的案例,虽然 h1 已经设置了 text 内容,但会在 3s 后才显示,因为 **JS执行和渲染都在浏览器的渲染主线程上执行**,在执行了 h1 内容设置后,向消息队列(message queue)中插入了新的渲染任务,但需要在 delay 完成后,渲染主线程才会执行渲染。
57+
58+
```javascript
59+
var h1 = document.querySelector(...);
60+
var btn = document.querySelector(...);
61+
62+
// 死循环一段时间
63+
function delay(duration) {
64+
var start = Date.now();
65+
while(Date.now() - start < duration) {}
66+
}
67+
68+
btn.onclick = function() {
69+
h1.textContent = 'xxx';
70+
delay(3000);
71+
}
72+
```
73+
74+
75+
76+
## 4. 任务没有优先级,但消息队列有优先级
77+
78+
每个任务都有一个任务类型,同一类型的任务必须在一个队列,一个队列可能有多种类型的任务,在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
79+
80+
浏览器必须准备好一个**微队列**(microTask),微队列中的任务优先于所有其他任务执行。
81+
82+
随着浏览器复杂度急剧提升,W3C不再使用宏队列的说法。
83+
84+
目前 chrome 中至少包含了以下队列:
85+
86+
+ **微队列**[最高] 存放需要最快执行的任务
87+
+ **交互队列**[] 存放用户交互后产生的事件处理任务
88+
+ **延时队列**[] 存放定时器回调
89+
90+
添加任务到微队列的方式主要为:`Promise``MutationObserver`,如
91+
92+
```javascript
93+
// 立即添加函数到微队列
94+
Promise.resolve().then(函数)
95+
```
96+
97+
案例:
98+
99+
```javascript
100+
function delay(duration) {
101+
var start = Date.now();
102+
while(Date.now() - start < duration) {}
103+
}
104+
setTimeout(function() { // 添加到延迟队列
105+
console.log(1);
106+
}, 0);
107+
Promise.resolve().then(function() { // 添加到微队列
108+
console.log(2);
109+
});
110+
delay(1000); // 死循环1s
111+
console.log(3); // 当前任务中执行
112+
// 1s 后输出 3 2 1
113+
```
114+
115+
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# 浏览器渲染原理
2+
3+
## 1. 渲染过程
4+
5+
![](./img/02.jpg)
6+
7+
### 1.1 【解析 HTML】 为 DOM 和 CSSOM
8+
9+
首先解析 HTML,解析过程中遇到 CSS 解析 CSS,遇到 JS 执行 JS。为提高解析效率,浏览器在开始解析前,会启动一个预解析线程,率先下载 HTML 中的外部 CSS 文件和 JS 文件。
10+
11+
若主线程解析到 link 位置时 CSS 文件还未下载解析完成则会跳过。因为下载和解析 CSS 是在预解析线程中进行的,所以 **CSS 不会阻塞渲染主线程对 HTML 的解析**
12+
13+
主线程解析到 script 位置时会停止解析 HTML,等待 JS 文件下载结束,并将全局代码解析执行完成后,才继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停,所以 **JS 会阻塞渲染主线程对 HTML 的解析**
14+
15+
解析 HTML 完成后,会得到 **DOM 树****CSSOM 树**,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。
16+
17+
### 1.2 【样式计算】得到带样式的 DOM
18+
19+
主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出最终样式,称之为 **Computed Style**。在此过程中,很多预设值会变成绝对值,比如 red 会变成 rgb(255,0,0); 相对单位会变成绝对单位,比如 em 会变成 px。
20+
21+
样式计算完成后,会得到一棵带有样式的 DOM 树。
22+
23+
> js 操作 stylesheet
24+
>
25+
> ```javascript
26+
> document.styleSheets
27+
> getComputedStyle(document.body)
28+
> ```
29+
30+
### 1.3 【布局】生成 layout 布局树
31+
32+
布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息,如节点的宽高、相对包含块的位置。
33+
34+
大部分时候,DOM 树和布局树并非一一对应。比如 `display:none` 节点不会生成到布局树;比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中;还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。
35+
36+
![](./img/03.jpg)
37+
38+
### 1.4 【分层】
39+
40+
主线程会使用一套复杂的策略对整个布局树进行分层。分层会影响后续的布局树变化的处理,当某层变动只改某层能提升性能,但层次过多也会消耗性能。滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,可以通过 `will-change` 属性更大程度的影响分层结果,如在css中添加 `will-change: transform` 属性告诉浏览器将来 transform 可能变化,由浏览器决定是否分层。
41+
42+
### 1.5 【绘制】生成绘制指令集传递给合成线程
43+
44+
主线程会为每个层单独生成绘制指令集,完成后主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。
45+
46+
### 1.6 【分块】合成线程分块
47+
48+
合成线程首先将每个图层划分为更多的小区域,它会从线程池中拿取多个线程来完成分块工作。
49+
50+
### 1.7 【光栅化】GPU光栅化
51+
52+
合成线程会将块信息交给 GPU 进程,GPU 进程会开启多个线程极速完成光栅化,得到一块一块的位图,默认会优先处理靠近视口区域的块。
53+
54+
### 1.8 【draw】合成线程根据位图生成指引并由GPU绘制到屏幕
55+
56+
合成线程拿到每个层、每个块的位图后,生成一个个"指引(quad)"信息,用于标识出每个位图在屏幕的绘制位置,以及旋转、缩放等变形的处理。
57+
58+
由于 `transform` 变形发生在合成线程,与渲染主线程无关,所以效率高。
59+
60+
最后合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。
61+
62+
63+
64+
## 2. transform 高效的原因
65+
66+
如前面说述,transform 既不影响布局也不影响绘制指令,只影响渲染流程最后的绘制阶段。由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响忙碌的渲染主线程。
67+
68+
69+
70+
## 3. reflow(重排) & repaint(重绘)
71+
72+
`reflow`:当进行了影响布局树的操作后,需要重新计算 layout 布局树。浏览器会合并布局属性的修改操作,当 JS 代码全部完成后再异步统一计算。但为了让 JS 获取布局属性时取到最新的布局信息,在获取属性时会立即 reflow。
73+
74+
`repaint`:当修改了可见样式后需要重新计算绘制指令。
75+
76+
由于元素的布局信息也属于可见样式,所以 **reflow 一定会引起 repaint**,要尽量减少 reflow。
77+
78+
![](./img/04.jpg)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# 属性描述符
2+
3+
## 1. 属性描述符的获取与设置
4+
5+
```javascript
6+
var obj = { a: 1, b: 2 };
7+
8+
// 获取属性描述符
9+
Object.getOwnPropertyDescriptor(obj, 'a');
10+
// { value: 1, writable: true, enumerable: true, configurable: true }
11+
12+
// 设置属性描述符
13+
Object.defineProperty(obj, 'a', {
14+
// value: 3, // Cannot both specify accessors and a value or writable attribute
15+
// writable: false, // 不可重写
16+
enumerable: false, // 不可遍历
17+
configurable: false, // 属性描述符不可再修改
18+
get: function() { // getter
19+
return 4;
20+
},
21+
set: function() { // setter
22+
throw new Error('属性只读');
23+
},
24+
});
25+
```
26+
27+
## 2. 案例:属性描述符的应用
28+
29+
```javascript
30+
class Demo {
31+
constructor (g) {
32+
g = { ...g };
33+
Object.freeze(g); //【1】clone后冻结,避免改变原对象
34+
Object.defineProperty(this, 'data', {
35+
get: function() {
36+
return g;
37+
},
38+
set: function() {
39+
throw new Error('data 属性只读');
40+
},
41+
configurable: false, // 属性描述符不可再修改
42+
});
43+
44+
this.testForFreeze = 1;
45+
// Object.freeze(this); //【2】冻结,但也会影响现有的属性方法
46+
Object.seal(this); //【3】密封,不能给对象添加/删除属性和方法,不能修改现有属性和方法的配置
47+
}
48+
}
49+
50+
var g = new Demo({
51+
price: 1,
52+
});
53+
54+
g.data.price = 2;
55+
g.testForFreeze = 2; // Object.freeze 后不会生效,但 Object.seal 不影响
56+
g.testForSeal = 2; // Object.freeze 和 Object.seal 后都不会生效
57+
58+
Object.freeze(Demo.prototype); //【4】冻结原型链
59+
Demo.prototype.testForPrototype = 1;
60+
console.log(g.testForPrototype); // undefined
61+
```
62+
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# CSS包含块
2+
3+
元素的尺寸和位置,会受它的包含块影响。对于一些属性,例如 width, height, padding, margin,当绝对定位元素(position: absolute/fixed)的偏移值为百分比值时,由元素的包含块计算得来。
4+
5+
包含块分两种:
6+
7+
+ **初始包含块(initial containing block)**,即根元素(HTML元素)所在的包含块,浏览器中等于视口 viewport 大小,定位基点在画布原点(视口左上角)。
8+
9+
+ **非根元素包含块**,判定方式分以下规则:
10+
11+
1. 元素 `position: relative/static`,包含块为最近的**块容器** (block container) 的**内容区域**(content area)
12+
13+
2. 元素 `position: fixed`,包含块为视口
14+
15+
3. 元素 `position: absolute`,包含块为最近的 `position` 的值非 `static` (即值为 `fixed`/`absolute`/`relative`/`sticky`) 的祖先元素的内边距区域
16+
17+
案例:
18+
19+
```html
20+
<body>
21+
<div class="container" style="position: relative;">
22+
<div class="item">
23+
<div class="item2" style="position: absolute;"></div>
24+
</div>
25+
</div>
26+
</body>
27+
```
28+
29+
`div.item2` 的包含块为 `div.container` 而非 `div.item`
30+
31+
此外,对于非根元素,包含块还有一种规则:元素 `position: absolute/fixed`,包含块也可能为满足以下条件的最近父级元素的内边距区域:
32+
33+
- `transform``perspective` 值不为 `none`
34+
- `will-change: transform/perspective;`
35+
- `filter` 值不为 `none``will-change: filter;`(仅 Firefox)
36+
- `contain: paint;`
37+
38+
案例:
39+
40+
```html
41+
<body>
42+
<div class="container" style="position: relative;">
43+
<div class="item" style="transform: rotate(0deg);">
44+
<div class="item2" style="position: absolute;"></div>
45+
</div>
46+
</div>
47+
</body>
48+
```
49+
50+
此时,`div.item2` 的包含块为 `div.item`
51+
52+
53+
54+
CSS 规范中所举的例子如下:
55+
56+
```html
57+
<html>
58+
<head>
59+
<title>Illustration of containing blocks</title>
60+
</head>
61+
<body id="body">
62+
<div id="div1">
63+
<p id="p1">This is text in the first paragraph...</p>
64+
<p id="p2">
65+
This is text
66+
<em id="em1">
67+
in the
68+
<strong id="strong1">second</strong>
69+
paragraph.
70+
</em>
71+
</p>
72+
</div>
73+
</body>
74+
</html>
75+
```
76+
77+
在没有添加任何 CSS 代码的情况下,元素对应的包含块为:
78+
79+
| 元素 | 包含块 | 参照规则 | 说明 |
80+
| ------- | --------------------------- | -------- | ------------------ |
81+
| html | initial C.B. (UA-dependent) | 0 | |
82+
| body | html | 1 | |
83+
| div1 | body | 1 | |
84+
| p1 | div1 | 1 | |
85+
| p2 | div1 | 1 | |
86+
| em1 | p2 | 1 | |
87+
| strong1 | **p2** | 1 | 最近块容器的内容区 |
88+
89+
接下来添加如下 CSS:
90+
91+
```css
92+
#div1 {
93+
position: absolute;
94+
left: 50px;
95+
}
96+
```
97+
98+
变化如下:
99+
100+
| 元素 | 包含块 | 参照规则 | 说明 |
101+
| ---- | --------------------------- | -------- | -------------------------------- |
102+
| div1 | initial C.B. (UA-dependent) | 3 | 最近的非static祖先元素内边距区域 |
103+
104+
继续修改 CSS:
105+
106+
```css
107+
#div1 {
108+
position: absolute;
109+
left: 50px;
110+
}
111+
#em1 {
112+
position: absolute;
113+
left: 100px;
114+
}
115+
```
116+
117+
变化如下:
118+
119+
| 元素 | 包含块 | 参照规则 | 说明 |
120+
| ------- | --------------------------- | -------- | -------------------------------- |
121+
| div1 | initial C.B. (UA-dependent) | | |
122+
| em1 | div1 | 3 | 最近的非static祖先元素内边距区域 |
123+
| strong1 | em1 | 1 | 最近块容器的内容区 |
124+
125+
其他参考案例:*https://developer.mozilla.org/zh-CN/docs/Web/CSS/Containing_block*
126+
127+
128+
129+
130+
131+
132+

0 commit comments

Comments
 (0)