- Published on
渲染帧与事件循环与react concurrent
- Authors
- Name
- Yanbin
- @ybtaimu
事件循环与一帧的关系
在一帧的时间内
,浏览器会尽可能地执行JavaScript代码
并在一帧末尾进行页面渲染
。 而不是在执行每段JavaScript代码后都进行渲染
。即并不是每一轮消息循环结束时都会产生一个渲染任务
。 这要根据屏幕刷新率、页面性能、页面是否在后台运行等来共同决定,通常来说这个渲染间隔是固定的。通常决定渲染时机的因素有如下几点:
- 显示器一般帧率为 60fps(每 16.66ms 渲染一次),如果页面性能维持不了 60fps,浏览器会降到 30fps 以保证渲染能够进行下去。
- 如果浏览器上下文不可见,那么页面会降低到 4fps 左右甚至更低。
- 如果浏览器判定当前改动不会引起视觉变化或者 requestAnimationFrame 回调为空时,则会跳过渲染。
- 在页面 resize、页面 scroll、requestAnimationFrame 调用、IntersectionObserver 触发显示、元素显示、隐藏或结构变化时,一般在渲染间隔到来时会推送一个渲染任务。
浏览器一帧的组成
对于此图浏览器并不需要执行所有步骤
,具体情况取决于哪些步骤是必需的。例如,如果没有新的 HTML 要解析,那么解析 HTML 的步骤就不会触发。
一帧生命流程详细解释
1.开始新的一帧
。垂直同步信号触发,开始渲染新的一帧图像。
2.事件的处理
。从合成线程将输入的数据,传递到主线程的事件处理函数.
3.requestAnimationFrame
。此处适合做动画或更新屏幕显示内容
4.解析 HTML(Parse HTML)
5.重新计算样式(Recalc Styles)
。为新添加或变更的内容计算样式
6.布局(Layout)
。计算每个可见元素的几何信息(每个元素的位置和大小)。一般作用于整个文档,计算成本通常和 DOM 元素的大小成比例。
7.更新图层树(Update Layer Tree)
。创建层叠上下文,为元素的深度进行排序
8.Paint
。过程分为两步:第一步,对所有新加入的元素,或进行改变显示状态的元素,记录 draw 调用(这里填充矩形,那里写点字);第二步是栅格化(Rasterization,见后文),在这一步实际执行了 draw 的调用
,并进行纹理填充。Paint 过程记录 draw 调用
,一般比栅格化要快,但是两部分通常被统称为“painting”。
9.合成(Composite)
。图层和图块信息计算完成后,被传回合成线程进行处理。这将包括 will-change、重叠元素和硬件加速的 canvas 等。
10.栅格化规划(Raster Scheduled)和栅格化(Rasterize)
。在 Paint 任务中记录的 draw 调用在此步骤执行。
11.帧结束
。各个层的所有的块都被栅格化成位图后,新的块和输入数据(可能在事件处理程序中被更改过)被提交给 GPU 线程。
12.requestIdleCallback(若还有剩余时间)
。在帧结束时,主线程还有点时间,requestIdleCallback 可能会被触发。
13.发送帧
。图块被 GPU 线程上传到 GPU。GPU 使用四边形和矩阵(所有常用的 GL 数据类型)将图块 draw 在屏幕上
两种图层(拓展阅读)
在工作流程中深度的排序有两种版本。
首先是层叠上下文
,比如有 2 个绝对定位的重叠的 div。更新图层树(Update Layer Tree) 是流程的一部分,保证 z-index 和类似的属性受到重视。
然后是合成图层
,合成线程负责计算出每一个位图在屏幕上的位置,交给 GPU 进行最终呈现。 这里来解释一下,渲染进程实际上是在沙盒里边运行的,其没有操作硬件的能力,所以这里必须要交给 GPU 进程过渡。 比较神奇的是,css 样式 transform
并不是在光栅化中生成像素点,而是在这一步真正要画的时候决定的,就是在 GPU 做一个矩阵变换
。
注意点
reflow(回流)是什么?
回流的本质是重新计算布局树
。当进行了影响布局的操作后(cssom改变),会引发一次 layout,如下图
开发过程中,代码应该避免频繁修改布局树(height/width/margin/padding...)
repaint(重绘)是什么 ?
本质是重新根据分层信息计算绘制的指令,回流一定会引起重绘
为什么 transform 效率高 ?
计算 transform 是在最后一步(draw)时,针对本层级的元素,在 GPU 中执行变换
的,不会引起回流和重绘
。requestAnimationFrame 的回调有两个特点
- 在重新渲染前调用。 保证了在渲染任务执行之前执行完想要改变的元素,保证了动画的流畅,不会拖延到下一帧再渲染
- 回调合并执行。 多个requestAnimationFrame的callback函数会在同一帧内执行,而不是放到下一帧执行。
requestIdleCallback
如果浏览器的工作比较繁忙
的时候,不能保证
它会提供空闲时间去执行 rIC 的回调
,而且可能会长期的推迟下去。所以如果你需要保证你的任务在一定时间内一定要执行掉,那么你可以给 rIC 传入第二个参数 timeout。 这会强制浏览器不管多忙,都在超过这个时间之后去执行 rIC 的回调函数。所以要谨慎使用,因为它会打断浏览器本身优先级更高的工作。
拓展的一些关于渲染相关的问题
渲染帧与JavaScript
function btn() {
console.log('test btn')
// btn(); 第一种情况
// setTimeout(btn,0) 第二种情况
// Promise.resolve().then(btn) 第三种情况
}
const normal = document.getElementById("normal")
normal.addEventListener('click', btn)
上面的三种死循环 UI表现如何 会卡死嘛?
1.针对第一种情况。同步任务,直接调用btn函数会导致同步的无限递归,这个操作会迅速耗尽调用栈空间,在很短的时间内引发“RangeError: Maximum call stack size exceeded”错误。
2.针对第二种情况。task任务队列,UI可正常滚动 不会抛出栈溢出异常。setTimeout 引入了延迟,将下一个btn调用放到事件循环队列
中,而不是直接递归调用。这允许浏览器在两次调用之间处理其他事件
,包括UI事件,比如滚动和渲染
。因此,即使btn函数持续调用,页面也不会立即卡死。
3.针对第二种情况。micro-task任务对列,通过Promise不断地以递归方式创建微任务,无限递归的生成微任务 微任务队列永远不会为空 js线程会一直执行微任务
浏览器没有机会去执行其他宏任务
,如UI渲染或事件监听器的调用, 这会导致UI渲染被阻塞
,界面无法响应用户操作,体验到的结果就如同UI卡死一样。
Recap:
1.task:JavaScript引擎使用事件循环来管理异步操作
,task任务在每次事件循环迭代中执行,task任务之间会有断点,每个task任务完成
后,调用栈都会清空,然后处理下一个宏任务。这意味着即使是连续安排的多个宏任务,也不会导致调用栈累积而溢出。 2.microtask:微任务在当前task任务完成后、下一个task任务开始前执行。虽然微任务是连续执行的,但每个微任务都是独立入栈的;即使它们形成了长队列,每次只处理一个微任务,处理完后就从调用栈中弹出。因此,即使微任务队列很长,每次执行完毕都会清空调用栈,不会累积导致栈溢出
Quoter:
https://juejin.cn/post/7355063847382810635#heading-2
并发模型与事件循环
JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。
多个运行时互相通信
一个 web worker
或者一个跨域的 iframe
都有自己的栈、堆和消息队列
。两个不同的运行时
只能通过 postMessage 方法进行通信
。如果另一个运行时侦听 message 事件,则此方法会向该运行时添加消息。
React concurrent原理
React concurrent模式的原理便是通过requestAnimationFrame 与 MessageChannel 实现的
。