Skip to content

Commit 963f41d

Browse files
author
Yanbin Zhu
committed
feat(*): 浏览器渲染原理与事件循环
1 parent ab325d8 commit 963f41d

File tree

11 files changed

+291
-7
lines changed

11 files changed

+291
-7
lines changed

data/blog/browser/architecture.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Preconditions:Chrome Version >= 72
7575
- [Google官方blog](https://developer.chrome.com/blog/inside-browser-part1?hl=zh-cn)
7676

7777
下面几个链接的内容差不多
78+
- [渲染原理](https://juejin.cn/post/7293820517375623168#heading-0)
7879
- [详细内容(部分已过时)](https://github.com/yacan8/blog/issues/28)
7980
- https://juejin.cn/post/6844904046411644941
8081
- https://juejin.cn/post/6844904158131126279?from=search-suggest

data/blog/browser/eventloop.mdx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: Evnet Loop事件循环
33
date: '2024-08-11'
44
tags: ['frontend', 'browser','Evnet Loop']
55
draft: false
6-
summary: 浏览器 及 node 的Evnet Loop
6+
summary: 浏览器 及 node 的Evnet Loop,及常见异步task
77
---
88

99

@@ -26,6 +26,25 @@ summary: 浏览器 及 node 的Evnet Loop
2626
延迟队列:用于存放定时器到达后的回调任务,优先级次于交互队列
2727
```
2828

29+
```
30+
根据W3C最新标准,现代浏览器将宏任务队列的概念进一步做了细分。原来的宏任务主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、requestAnimationFrame、requestIdleCallback、setImmediate(Node.js 环境)等。
31+
32+
注意:此处的MessageChannel即是React用来与requestAnimationFrame()用来模拟requestIdleCallback的api。
33+
requestAnimationFrame 也是天然的异步任务。
34+
```
35+
[常见异步任务](https://juejin.cn/post/7342512113170890764#heading-2)
36+
37+
38+
## 理解异步
39+
线程语言是没有严格意义的异步的,代码是从上至下依次执行的。上面讲的异步,是在浏览器维度来讲的,`js没有多线程`,但是`浏览器可以开辟多个线程``维护异步的任务`
40+
41+
42+
渲染主线程执行 js 任务(js线程阻塞执行),主线程有承担渲染的其他任务,如果都同步执行,主线程就会白白消耗时间,页面经常性假死。
43+
44+
45+
采用消息循环+异步时,在`异步任务发生时`,主线程`将这个任务交给其他线程来执行``跳过这个任务往后执行``其他线程执行完毕后将回调包装成任务放入消息队列末尾``等待主线程调用`
46+
47+
2948
# node.js Event Loop:
3049

3150
与在浏览器中一样,在 nodejs 中 JS 最开始在主线程上执行,执行同步任务、发出异步请求、规划定时器生效时间、执行 process.nextTick 等,这时事件循环还没开始。
@@ -79,7 +98,11 @@ libuv 会以异步的方式将任务的执行结果返回给 V8 引擎,V8 引
7998
2.nextTick 队列的优先级还要高于 Promise 队列,所以 process.nextTick 是 nodejs 中执行最快的异步操作
8099

81100

82-
# Quoter:
101+
# Quoter:(推荐度从上往下)
102+
- [较新较详细的浏览器事件循环文章](https://juejin.cn/post/7288956366515929088?from=search-suggest)
103+
104+
- [事件循环-比较详细-推荐](https://juejin.cn/post/6844903764202094606#heading-13)
105+
83106
- [事件循环最新文章](https://juejin.cn/post/7326803868326592539)
84107

85108
- [此资料中关于node-eventloop的资料已经过时](https://juejin.cn/post/6844903761949753352#heading-1)

data/blog/browser/frame.mdx

Lines changed: 143 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,151 @@
11
---
22
title: 渲染帧与事件循环与react concurrent
3-
date: '2024-08-11'
4-
tags: ['browser', 'frontend','web worker','service worker']
5-
draft: true
6-
summary: web worker 及 service worker 相关理解
3+
date: '2024-08-15'
4+
tags: ['browser', 'frontend','frame','concurrent']
5+
summary: 渲染帧与事件循环与react concurrent
76
---
87

8+
# 事件循环与一帧的关系
9+
`一帧的时间内`,浏览器会`尽可能地执行JavaScript代码``在一帧末尾进行页面渲染`
10+
`不是在执行每段JavaScript代码后都进行渲染`。即`并不是每一轮消息循环结束时都会产生一个渲染任务`
11+
这要根据屏幕刷新率、页面性能、页面是否在后台运行等来共同决定,通常来说这个渲染间隔是固定的。通常决定渲染时机的因素有如下几点:
912

13+
- 显示器一般帧率为 60fps(每 16.66ms 渲染一次),如果页面性能维持不了 60fps,浏览器会降到 30fps 以保证渲染能够进行下去。
14+
- 如果浏览器上下文不可见,那么页面会降低到 4fps 左右甚至更低。
15+
- 如果浏览器判定当前改动不会引起视觉变化或者 requestAnimationFrame 回调为空时,则会跳过渲染。
16+
- 在页面 resize、页面 scroll、requestAnimationFrame 调用、IntersectionObserver 触发显示、元素显示、隐藏或结构变化时,一般在渲染间隔到来时会推送一个渲染任务。
1017

1118

19+
# 浏览器一帧的组成
20+
<Image className="w-full" width={300} height={150} src="/static/images/browser/anatomy-of-a-frame.svg" alt="anatomy-of-a-frame"/>
21+
对于此图`浏览器并不需要执行所有步骤`,具体情况取决于哪些步骤是必需的。例如,如果没有新的 HTML 要解析,那么解析 HTML 的步骤就不会触发。
1222

13-
事件循环与时间切片(https://segmentfault.com/a/1190000044483894#item-4-4)
23+
## 一帧生命流程详细解释
24+
1.`开始新的一帧`。垂直同步信号触发,开始渲染新的一帧图像。
25+
26+
2.`事件的处理`。从合成线程将输入的数据,传递到主线程的事件处理函数.
27+
28+
3.`requestAnimationFrame`。此处适合做动画或更新屏幕显示内容
29+
30+
4.`解析 HTML(Parse HTML)`
31+
32+
5.`重新计算样式(Recalc Styles)`。为新添加或变更的内容计算样式
33+
34+
6.`布局(Layout)`。计算每个可见元素的几何信息(每个元素的位置和大小)。一般作用于整个文档,计算成本通常和 DOM 元素的大小成比例。
35+
36+
7.`更新图层树(Update Layer Tree)`。创建层叠上下文,为元素的深度进行排序
37+
38+
8.`Paint`。过程分为两步:第一步,对所有新加入的元素,或进行改变显示状态的元素,记录 draw 调用(这里填充矩形,那里写点字);第二步是栅格化(Rasterization,见后文),在`这一步实际执行了 draw 的调用`,并进行纹理填充。`Paint 过程记录 draw 调用`,一般比栅格化要快,但是两部分通常被统称为“painting”。
39+
40+
9.`合成(Composite)`。图层和图块信息计算完成后,被传回合成线程进行处理。这将包括 will-change、重叠元素和硬件加速的 canvas 等。
41+
42+
10.`栅格化规划(Raster Scheduled)和栅格化(Rasterize)`。在 Paint 任务中记录的 draw 调用在此步骤执行。
43+
44+
11.`帧结束`。各个层的所有的块都被栅格化成位图后,新的块和输入数据(可能在事件处理程序中被更改过)被提交给 GPU 线程。
45+
46+
12.`requestIdleCallback(若还有剩余时间)`。在帧结束时,主线程还有点时间,requestIdleCallback 可能会被触发。
47+
<Image className="w-full" width={300} height={150} src="/static/images/browser/execute-of-requestIdleCallback.jpg" alt="execute-of-requestIdleCallback"/>
48+
49+
13.`发送帧`。图块被 GPU 线程上传到 GPU。GPU 使用四边形和矩阵(所有常用的 GL 数据类型)将图块 draw 在屏幕上
50+
51+
52+
53+
## 两种图层(拓展阅读)
54+
在工作流程中深度的排序有两种版本。
55+
56+
首先是`层叠上下文`,比如有 2 个绝对定位的重叠的 div。更新图层树(Update Layer Tree) 是流程的一部分,保证 z-index 和类似的属性受到重视。
57+
58+
然后是`合成图层`,合成线程负责计算出每一个位图在屏幕上的位置,交给 GPU 进行最终呈现。
59+
这里来解释一下,渲染进程实际上是在沙盒里边运行的,其没有操作硬件的能力,所以这里必须要交给 GPU 进程过渡。
60+
比较神奇的是,css 样式 `transform` 并不是在光栅化中生成像素点,而是在这一步真正要画的时候决定的,就是`在 GPU 做一个矩阵变换`
61+
62+
63+
## 注意点
64+
1. `reflow(回流)是什么?`
65+
回流的本质是`重新计算布局树`。当进行了影响布局的操作后(cssom改变),会引发一次 layout,如下图
66+
<Image className="w-full" width={300} height={150} src="/static/images/browser/reflow.jpg" alt="reflow"/>
67+
开发过程中,代码应该避免频繁修改布局树(height/width/margin/padding...)
68+
69+
2. `repaint(重绘)是什么 ?`
70+
本质是重新根据分层信息计算绘制的指令,`回流一定会引起重绘`
71+
<Image className="w-full" width={300} height={150} src="/static/images/browser/repaint.jpg" alt="repaint"/>
72+
73+
74+
3. `为什么 transform 效率高 ?`
75+
计算 transform 是在最后一步(draw)时,针对本层级的元素,`在 GPU 中执行变换`的,`不会引起回流和重绘`
76+
77+
4. `requestAnimationFrame 的回调有两个特点`
78+
1. 在重新渲染前调用。
79+
保证了在渲染任务执行之前执行完想要改变的元素,保证了动画的流畅,不会拖延到下一帧再渲染
80+
2. 回调合并执行。
81+
多个requestAnimationFrame的callback函数会在同一帧内执行,而不是放到下一帧执行。
82+
83+
5. `requestIdleCallback`
84+
如果`浏览器的工作比较繁忙`的时候,`不能保证`它会`提供空闲时间去执行 rIC 的回调`,而且可能会长期的推迟下去。所以如果你需要保证你的任务在一定时间内一定要执行掉,那么你可以给 rIC 传入第二个参数 timeout。
85+
这会强制浏览器不管多忙,都在超过这个时间之后去执行 rIC 的回调函数。所以要谨慎使用,因为它会打断浏览器本身优先级更高的工作。
86+
87+
88+
### [拓展的一些关于渲染相关的问题](https://juejin.cn/post/6998154127763046437#heading-8)
89+
90+
91+
# 渲染帧与JavaScript
92+
93+
```JavaScript
94+
function btn() {
95+
console.log('test btn')
96+
// btn(); 第一种情况
97+
// setTimeout(btn,0) 第二种情况
98+
// Promise.resolve().then(btn) 第三种情况
99+
}
100+
const normal = document.getElementById("normal")
101+
normal.addEventListener('click', btn)
102+
```
103+
104+
上面的三种死循环 UI表现如何 会卡死嘛?
105+
106+
1.针对第一种情况。同步任务,直接调用btn函数会导致同步的无限递归,这个操作会迅速耗尽调用栈空间,在很短的时间内引发“RangeError: Maximum call stack size exceeded”错误。
107+
<Image className="w-full" width={300} height={150} src="/static/images/browser/sync_frame.jpg" alt="sync_frame"/>
108+
109+
110+
2.针对第二种情况。task任务队列,UI可正常滚动 不会抛出栈溢出异常。setTimeout 引入了延迟,将下一个btn调用`放到事件循环队列`中,而不是直接递归调用。这允许浏览器在`两次调用之间处理其他事件`,包括`UI事件,比如滚动和渲染`。因此,即使btn函数持续调用,页面也不会立即卡死。
111+
<Image className="w-full" width={300} height={150} src="/static/images/browser/task_frame.jpg" alt="task_frame"/>
112+
113+
3.针对第二种情况。micro-task任务对列,通过Promise不断地以递归方式创建微任务,无限递归的生成微任务 微任务队列永远不会为空 `js线程会一直执行微任务` 浏览器`没有机会去执行其他宏任务`,如UI渲染或事件监听器的调用, 这会`导致UI渲染被阻塞`,界面无法响应用户操作,体验到的结果就如同UI卡死一样。
114+
<Image className="w-full" width={300} height={150} src="/static/images/browser/microtask_frame.jpg" alt="microtask_frame"/>
115+
116+
117+
118+
## Recap:
119+
1.task:JavaScript引擎使用`事件循环来管理异步操作`,task任务在每次事件循环迭代中执行,task任务之间会有断点,每个`task任务完成`后,调用栈都会清空,然后处理下一个宏任务。这意味着即使是连续安排的多个宏任务,也不会导致调用栈累积而溢出。
120+
2.microtask:微任务在当前task任务完成后、下一个task任务开始前执行。虽然微任务是连续执行的,但每个微任务都是独立入栈的;即使它们形成了长队列,每次只处理一个微任务,处理完后就从调用栈中弹出。因此,即使微任务队列很长,每次执行完毕都会清空调用栈,不会累积导致栈溢出
121+
122+
## Quoter:
123+
https://juejin.cn/post/7355063847382810635#heading-2
124+
125+
126+
# 并发模型与事件循环
127+
128+
JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。
129+
130+
## 多个运行时互相通信
131+
一个` web worker` 或者一个跨域的 `iframe``有自己的栈、堆和消息队列`。两个`不同的运行时`只能通过 `postMessage 方法进行通信`。如果另一个运行时侦听 message 事件,则此方法会向该运行时添加消息。
132+
133+
134+
## React concurrent原理
135+
`React concurrent模式的原理便是通过requestAnimationFrame 与 MessageChannel 实现的`
136+
137+
138+
139+
140+
141+
142+
# Quoter
143+
- [fram组成-cn](https://juejin.cn/post/6844903808762380296?searchId=202401021707295093F1858BA28B159756)
144+
145+
- [frame组成-en](https://aerotwist.com/blog/the-anatomy-of-a-frame/)
146+
147+
- [渲染原理-推荐](https://juejin.cn/post/7293820517375623168#heading-0)
148+
149+
- [浏览器原理](https://juejin.cn/post/6844903684665507848?from=search-suggest#heading-3)
150+
151+
- [事件循环与时间切片](https://segmentfault.com/a/1190000044483894#item-4-4)

0 commit comments

Comments
 (0)