一、React 设计理念
00 min
2024-10-12

 
软件的设计服务于理念。只有理解设计理念,才能明白如何架构以实现这一理念。
所以,在我们深入源码架构之前,先来聊一聊React理念。
 
从官网可以看到 React 是这么介绍的:
我们认为,React 是基于 JavaScript 构建快速响应的大型Web应用程序的首先方式。它在 Facebook 和 Instagram 上表现优秀。
可见,关键是实现快速响应,那么制约快速响应的因素是什么呢?
  • 当遇到大计算量的操作或者是设备性能不足时,会导致页面的掉帧和卡顿。
  • 发送网络请求后,需要等待数据返回才能进一步操作,导致不能快速响应。
这两种场景可以分别概括为 CPU 的瓶颈(计算能力)和 IO 的瓶颈(网络延迟),那么 React 是如何解决这两个瓶颈的呢?
 

1.1 CPU 的瓶颈


当项目变得愈发庞大、组件越来越多的时候,就容易遇到 CPU 的瓶颈。
试想我们向视图中渲染 3000 个 li:
主流浏览器的刷新率为 60Hz,即每秒(1000ms / 60Hz)16.6ms 刷新一次。
我们知道 JS 是可以操作 DOM 的,但 GUI 渲染线程JS 线程是互斥的,所以 JS 脚本执行浏览器布局、绘制是不能同时执行的。
在符合预期的一帧(16.6ms) 内,浏览器需要完成如下工作:
但当 JS 脚本执行时间过长超出 16.6ms,那在本次的刷新中就没有时间执行样式布局绘制了。
在上述的demo 中,由于组件数量繁多(3000 个),JS 脚本执行时间过长,页面掉帧造成卡顿。
可以从 Chrome 的 Performance 面板执行的堆栈图中看到,JS 执行时间为 73.65ms,这个时间远远多于一帧的时间。
notion image
React 如何解决这个问题的呢?
答案是:在浏览器每一帧的时间中,预留一些时间给 JS 线程,React 利用这部分时间更新组件(执行 JS 脚本)
💡
在 React 源码当中,预留的初始时间是 5ms
当预留的时间不够用时,React 会中断自己的工作并将线程控制权交还给浏览器,使其有时间渲染 UI,React 则等待下一帧时间到来继续被中断的工作。
 
这种将长任务拆分到每一帧中,像蚂蚁搬家一样一次执行一小段的任务操作,被称为时间切片(time slice)
 
接下来我们开启 Concurrent Mode (后续会详细讲解,暂时先了解如何开启时间切片)
此时,React 的长任务会被拆分到每一帧不同的 task 中,JS 脚本 执行时间大体在 5ms 左右,这样浏览器就有剩余时间去执行样式布局样式绘制了,减少掉帧的可能性。
notion image
所以,解决 CPU 瓶颈的关键是实现时间切片,而时间切片的关键是:将同步的更新变为异步可中断的更新
 
同步更新 vs 防抖 vs 节流 vs 异步更新对比 (Demo)
从 demo 中可以看到,当牺牲了列表的更新速度,React 大幅度提高了输入响应速度,让交互变得更加自然。
 

1.2 IO 的瓶颈


网络延迟是前端开发者无法解决的。如何在网络延迟客观存在的情况下呢,减少用户对网络延迟的感知呢?
以业界人机交互最顶尖的公司,苹果的 IOS 系统的表现为例:
点击“设置“面板中的”通用”,进入“通用”界面:
notion image
作为对比,再点击“设置“面板中的”Siri 与搜索“,进入”Siri 与搜索“界面:
notion image
可以感受到两者的在体验上的区别么?
事实上,点击“通用”后的交互是同步的,会直接显示后续界面。而在点击“Siri 与搜索”后的交互是异步的,需要等待请求返回后再显示后续界面。但是从用户的感知上看,两者的区别是微乎其微的。
这里的窍门在于:点击“Siri 与搜索”后,先在当前页面停留了一小段时间,这一小段时间被用来请求数据
当“这一小段时间”足够短时,用户是无感知的。如果请求时间超过了“这一小段时间”的范围,再显示一个 loading 的效果。
试想如果一点击“Siri 与搜索“就显示 loading 效果,即使请求时间很短,loading 效果一闪而过。用户也是可以感知到的。
为此,React 实现了 Suspense 功能及配套的 hook —— useDeferredValue
而在源码内部,为了支持这些特性,同样需要将同步的更新变为可中断的异步更新
 

1.3 总结


React 为了践行”构建快速响应的大型 Web 应用程序“理念做出的努力。
其中的关键是解决 CPU 瓶颈和 IO 的瓶颈。而落到实现上,则需要将同步的更新变为可中断的异步更新
 
下一节:
二、React 旧版架构
二、React 旧版架构
 
上一篇
空白文章
下一篇
解密 React:探究背后的原理与源码