|
1 | 1 | ---
|
2 | 2 | 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 |
7 | 6 | ---
|
8 | 7 |
|
| 8 | +# 事件循环与一帧的关系 |
| 9 | +在`一帧的时间内`,浏览器会`尽可能地执行JavaScript代码`并`在一帧末尾进行页面渲染`。 |
| 10 | +而`不是在执行每段JavaScript代码后都进行渲染`。即`并不是每一轮消息循环结束时都会产生一个渲染任务`。 |
| 11 | +这要根据屏幕刷新率、页面性能、页面是否在后台运行等来共同决定,通常来说这个渲染间隔是固定的。通常决定渲染时机的因素有如下几点: |
9 | 12 |
|
| 13 | +- 显示器一般帧率为 60fps(每 16.66ms 渲染一次),如果页面性能维持不了 60fps,浏览器会降到 30fps 以保证渲染能够进行下去。 |
| 14 | +- 如果浏览器上下文不可见,那么页面会降低到 4fps 左右甚至更低。 |
| 15 | +- 如果浏览器判定当前改动不会引起视觉变化或者 requestAnimationFrame 回调为空时,则会跳过渲染。 |
| 16 | +- 在页面 resize、页面 scroll、requestAnimationFrame 调用、IntersectionObserver 触发显示、元素显示、隐藏或结构变化时,一般在渲染间隔到来时会推送一个渲染任务。 |
10 | 17 |
|
11 | 18 |
|
| 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 的步骤就不会触发。 |
12 | 22 |
|
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