Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

宏观理解 React 原理 #32

Open
FE-Sadhu opened this issue Apr 9, 2024 · 0 comments
Open

宏观理解 React 原理 #32

FE-Sadhu opened this issue Apr 9, 2024 · 0 comments
Labels

Comments

@FE-Sadhu
Copy link
Owner

FE-Sadhu commented Apr 9, 2024

在不同领域帧有不同的含义,在视频与计算机动画领域中,我们看到的动画是由一张张静态图片组成,帧(frame)指的就是每一张静态的图片。
在人眼观看物体时,成像于视网膜,经由视神经传给人脑,感觉到物体的像,但当物体移去时,视神经对物体的印象不会立即消失,而要延续 0.1-0.4 秒的时间,人眼的这种性质被称为眼睛的视觉暂留

由于人眼的视觉暂留现象,所以当一张张静态图片快速消失出现时,我们会感觉到是动态的。
帧的度量叫帧率,用于测量显示帧数,测量单位为每秒显示帧数(frame per second, FPS)或赫兹。比如 60fps 代表每秒显示 60 帧。一般来说  FPS 用于描述视频、电子绘图或游戏每秒播放多少帧。
与 FPS 有个类似的概念叫做刷新率,单位为赫兹(Hz)。与 FPS 的区别是一般指屏幕每秒绘制新图像(帧)的次数。所以无论是 PC 显示器或是手机屏幕一定有一个刷新率的值。
负责渲染 UI 的应用就需要在刷新率的限制内按时提供每帧图像,如果提供得慢,跟不上刷新率,就会造成视觉上的卡顿。
举例说,浏览器就是一个有渲染 UI 功能的应用,如果此时硬件显示器是 60Hz 刷新率,1000ms / 60 = 16.6 ms,也就是浏览器需要在 16.6 ms 内完成输出一个画面(一帧),若未按时完成画面则叫做掉帧,帧率越低,人眼对掉帧的感觉越明显,越觉得卡顿。

// 利用 rAf 做实验验证浏览器没有自己的刷新率、最高匹配硬件刷新率的结论。 
let then = Date.now();
let count = 0;
const nextFrame = () => {
    requestAnimationFrame(() => {
        count++;
        if (count % 20 === 0) {
            const now = Date.now();
            const frame = ((now - then) / count).toFixed(2);  
            const fps =  (1000 / frame).toFixed(2);
            console.log('frame: ', frame, ' fps: ',fps);
        }
        nextFrame();
        
    })
}
nextFrame();

在浏览器输出每帧画面的过程中,会做以下事情:

可以看到,在一帧里,浏览器会依次处理用户输入事件、执行脚本、处理窗口变更、滚动、媒体查询、动画、执行 requestAnimationFrame 和 Intersection-Observer 回调、布局计算、绘制等。

设计理念

对于渲染 UI 应用而言,非快速响应就是掉帧。遇到设备性能不足或计算量大的任务,为了尽量避免掉帧,Web 框架能做的只能是避免每帧执行太多 JS 导致应用无法按时处理本帧的后续内容以及开启下一帧的工作。对此 React 给出的答卷是实现 时间分片(Time Slice) 技术。

把一个执行时间很长的长任务拆解为一个个执行时间很短的短任务,然后分别放在每一帧中执行的技术就是时间分片。

仅仅时间分片还不够,这只解决了掉帧问题,比如当前页面正在执行渲染十万行数据长列表这个长任务的一个个短任务,页面上还有个 input 输入框,实现时间分片后,浏览器能正常处理到每一帧中的 input events ,所以能及时收到用户输入。若无特殊策略,处理用户输入的响应会排队等待渲染十万行数据长列表的每个短任务执行完后再执行,但这通常是不符合用户体验的。
React 团队对于人机交互的研究成果表明,用户对不同操作的卡顿的感知程度不同。当用户在文本框输入内容时,即使从“键盘开始输入”到“文本框显示字符”之间只有轻微的延迟,也会使用户感觉到卡顿。但是当用户点击按钮加载数据时,即使从“点击按钮”到“显示数据”之间会经历数秒加载时间,用户也不会感觉到卡顿。
所以,React 又为每个更新任务划分了优先级,允许高优先级任务打断低优先级任务,高优先级任务执行完后接着执行低优先级任务。

架构

调度器、协调器、渲染器

上面讲述了对于 UI 非快速响应的用户体验问题,React 提出了时间分片与更新优先级的解决方案,具体来说,需要做三件事情:

  1. 为不同更新赋予不同优先级
  2. 调度不同更新以时间分片机制执行
  3. 如果更新正在进行(正在处理 Virtual DOM),有更高优先级的更新产生,则会中断当前更新,优先处理高优先级更新

要能做到以上 3 件事情,需要 React 底层实现:

  1. 用于调度优先级任务的调度算法
  2. 支持时间分片机制的调度器
  3. 支持处理可中断更新的协调器以及可中断的 Virtual Dom 实现。

由于 JS 跨平台的特性,理论上,React 处理 UI 的逻辑是跨平台通用的。各平台的使用区别应是拿到处理 UI 的结果后,调用各自宿主环境的操控真实 UI 的 API 将结果呈现在 UI 上。所以,React 也是为不同宿主环境写了对应的渲染器

举例说明

const Demo = () => {
	const [count, updateCount] = useState(0);

	return (
	<ul>
		<button onClick={() => updateCount(count + 1)}>乘以{count}</button>
		<li>{1 * count}</li>
		<li>{2 * count}</li>
		<li>{3 * count}</li>
	</ul>
	)
}

可中断的 Virtual Dom 实现

显卡的双缓冲机制

上述表明,UI 应用绘制的最终产物是一张图片,这张图片被发送给显卡后即可显示在屏幕上。显卡包含前缓冲区与后缓冲区。对于刷新率为 60Hz 的显示器,每秒会从前缓冲区读取 60 次图像,将其显示在显示器上。显卡的职责是:合成图像并写入后缓冲区。一旦后缓冲区被完整地写入图像,前后缓冲区就会互换,显示器再次从前缓冲区读取时就能读到一张完整的新图片,这就是显卡的双缓冲机制。

Fiber

Fiber 就是 React 里的 VDom 实现,不同的是,Fiber 除了描述 DOM 外,还包含了优先级更新机制相关信息。

Fiber 的工作原理类似于显卡的双缓冲机制。 当 React 有更新存在时,内存里有两棵 Fiber 树:current 树和 workInProgress 树。 current 树类比显卡的前缓冲区,对应真实 UI 显示,wip 树类比显卡的后缓冲区,所以的更新都会先在 wip 树里处理完成后,才提交给 renderer 渲染到 UI 上,最后切换 current 树与 wip 树。
所以,只要在本次更新生成的 wip 树没被提交给 renderer 之前,该 wip 树都是可以被覆盖或丢弃的,也就是本次更新被打断了。

架构实现

下篇会从源码讲解 React 是怎样实现这一套架构:剖析 React 内部运行机制

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant