React技术揭秘——理念

React理念

感觉就是看了看大概的系统理念 里面还有好多api什么的还没具体去看🤔

React理念

我们可以从官网看到React的理念:

我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式

可见,关键是实现快速响应。那么制约快速响应的因素是什么呢?

我们日常使用 App,浏览网页时,有两类场景会制约快速响应

  • 当遇到大计算量的操作或者设备性能不足使页面掉帧,导致卡顿。
  • 发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应。

这两类场景可以概括为:

  • CPU 的瓶颈
  • IO 的瓶颈

React是如何解决这两个瓶颈的呢?🤔

CPU的瓶颈

当项目变得庞大、组件数量繁多时,就容易遇到 CPU 的瓶颈。

考虑如下 🌰,我们向视图中渲染 3000 个li

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
const len = 3000;
return (
<ul>
{Array(len)
.fill(0)
.map((_, i) => (
<li>{i}</li>
))}
</ul>
);
}

const rootEl = document.querySelector("#root");
ReactDOM.render(<App />, rootEl);

主流浏览器刷新频率为 60Hz,即每(1000ms / 60Hz)16.6ms 浏览器刷新一次。

我们知道,JS 可以操作 DOM,GUI渲染线程JS线程是互斥的。所以JS 脚本执行浏览器布局、绘制不能同时执行。

在每 16.6ms 时间内,需要完成如下工作:

1
JS脚本执行 -----  样式布局 ----- 样式绘制

当 JS 执行时间过长,超出了 16.6ms,这次刷新就没有时间执行样式布局样式绘制了。

在 Demo 中,由于组件数量繁多(3000 个),JS 脚本执行时间过长,页面掉帧,造成卡顿。

如何解决这个问题呢?

答案是:在浏览器每一帧的时间中,预留一些时间给 JS 线程,React利用这部分时间更新组件(可以看到,在源码中,预留的初始时间是 5ms)。

当预留的时间不够用时,React将线程控制权交还给浏览器使其有时间渲染 UI,React则等待下一帧时间到来继续被中断的工作。

这种将长任务分拆到每一帧中,像蚂蚁搬家一样一次执行一小段任务的操作,被称为时间切片(time slice)

接下来我们开启Concurrent Mode(目前我知道开启后会启用时间切片😭):

1
2
3
// 通过使用ReactDOM.unstable_createRoot开启Concurrent Mode
// ReactDOM.render(<App/>, rootEl);
ReactDOM.unstable_createRoot(rootEl).render(<App />);

此时我们的长任务被拆分到每一帧不同的task中,JS脚本执行时间大体在5ms左右,这样浏览器就有剩余时间执行样式布局样式绘制,减少掉帧的可能性。

所以,解决CPU瓶颈的关键是实现时间切片,而时间切片的关键是:将同步的更新变为可中断的异步更新

IO的瓶颈

网络延迟是前端开发者无法解决的。如何在网络延迟客观存在的情况下,减少用户对网络延迟的感知?

React给出的答案是将人机交互研究的结果整合到真实的 UI 中

这里康康业界人机交互最顶尖的苹果如何处理的捏

在 IOS 系统中:

点击“设置”面板中的“通用”,进入“通用”界面

ios1

作为对比,再点击“设置”面板中的“Siri 与搜索”,进入“Siri 与搜索”界面:

能感受到两者体验上的区别么?

事实上,点击“通用”后的交互是同步的,直接显示后续界面。而点击“Siri 与搜索”后的交互是异步的,需要等待请求返回后再显示后续界面。但从用户感知来看,这两者的区别微乎其微。

这里的窍门在于:点击“Siri 与搜索”后,先在当前页面停留了一小段时间,这一小段时间被用来请求数据。

当“这一小段时间”足够短时,用户是无感知的。如果请求时间超过一个范围,再显示loading的效果。

试想如果我们一点击“Siri 与搜索”就显示loading效果,即使数据请求时间很短,loading效果一闪而过。用户也是可以感知到的。

为此,React实现了Suspense功能及配套的hook——useDeferredValue

(这两个还没看具体怎么用 只是知道有这样一个东西)

而在源码内部,为了支持这些特性,同样需要将同步的更新变为可中断的异步更新

React15

React从 v15 升级到 v16 后重构了整个架构。康康 v15,看看他为什么不能满足快速响应的理念,以至于被重构。

React15 架构

React15 架构可以分为两层:

  • Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面

Reconciler(协调器)

我们知道,在React中可以通过this.setStatethis.forceUpdateReactDOM.render等 API 触发更新。

每当有更新发生时,Reconciler会做如下工作:

  • 调用函数组件、或 class 组件的render方法,将返回的 JSX 转化为虚拟 DOM
  • 将虚拟 DOM 和上次更新时的虚拟 DOM 对比
  • 通过对比找出本次更新中变化的虚拟 DOM
  • 通知Renderer将变化的虚拟 DOM 渲染到页面上

Renderer(渲染器)

由于React支持跨平台,所以不同平台有不同的Renderer。我们前端最熟悉的是负责在浏览器环境渲染的Renderer —— ReactDOM

除此之外,还有:

  • ReactNative渲染器,渲染 App 原生组件
  • ReactTest渲染器,渲染出纯 Js 对象用于测试
  • ReactArt渲染器,渲染到 Canvas, SVG 或 VML (IE8)

在每次更新发生时,Renderer接到Reconciler通知,将变化的组件渲染在当前宿主环境。

React15 架构的缺点

Reconciler中,mount的组件会调用mountComponentupdate的组件会调用updateComponent。这两个方法都会递归更新子组件。

#递归更新的缺点

由于递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了 16ms,用户交互就会卡顿。

刚才已经提出了解决办法——用可中断的异步更新代替同步的更新。那么 React15 的架构支持异步更新么?

我们可以看到,ReconcilerRenderer是交替工作的,当第一个li在页面上已经变化后,第二个li再进入Reconciler

由于整个过程都是同步的,所以在用户看来所有 DOM 是同时更新的。

让我来试试,模拟一下,如果中途中断更新会怎么样?

当第一个li完成更新时中断更新,即步骤 3 完成后中断更新,此时后面的步骤都还未执行。

用户本来期望123变为246。实际却看见更新不完全的 DOM!(即223

基于这个原因,React决定重写整个架构。


React技术揭秘——理念
http://example.com/2024/10/06/React-底层/
Author
Shanyujia
Posted on
October 6, 2024
Licensed under