React 18 的一些新特性

最近太忙好久不写博客了, 欠了三篇啊!!!!!!!!!!!!!(以后有时间再补上)

本次博客, 是因为同事最近分享了React 18 新出的特性, 觉得长见识了,就一起学学习吧!!了解React 18新特性。

本文主要参考最近同事分享的一文让你搞懂React18新特性及其实现react18新特性及实践总结

Render API

我们知道, React 17 提供了 三种入口模式:

  • legacy模式:表示没有开启新功能,是React 17的默认模式。

    1
    ReactDOM.render(, rootNode)
  • blocking模式:是作为迁移到concurrent模式的过渡模式。

    1
    ReactDOM.createBlockingRoot(rootNode).render()
  • concurrent模式:这模式表示开启了所有的新功能。

    1
    ReactDOM.createRoot(rootNode).render()

React 18 正式迁移到了 concurrent 模式。但是同时, 用户还是可以继续使用React 17的下的旧API(只是会有警告提示)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// React 17
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

const root = document.getElementById('root')!;
ReactDOM.render(<App />, root);

// React 18
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = document.getElementById('root')!;
ReactDOM.createRoot(root).render(<App />);

Automatic Batching

批处理是React 将多个状态更新分组到一个渲染中一次获得更好的性能;即:React将多次的setState合并到一次进行渲染。

也就是说 setState 并不是试试修改State的,而是将多次的setState调用合并起来仅发出一次渲染,即可以减少数据状态存在中间值而导致的不稳定性,也可以提升渲染性能。

React 18 之前,只能在事件处理程序中批处理更新;举个例子如下:

注意:index.js 中使用的 Render 模式

以上例子中采用的是React 17 版本, 在事件处理程序(handleClick) 进行了两个setState。但我们可以看到 log 每次只打印一次。这就表明在事件处理程序中 React 17 采用了批处理更新。

但是在React 18 之前,异步函数(定时器, Promise 等)中的setState 并不会执行合并, 由于丢失上下文,无法做到合并处理,所以每次setState调用都会立即发出一次重新渲染,除了重复的setState。举个例子如下:

注意:index.js 中使用的 Render 模式

以上例子中采用的是React 17 版本,在异步事件处理程序(setTimeOut) 进行了两个setState。 可以看到每次 log 都会打印两次。说明在异步函数(定时器, Promise 等)中的setState 并不会执行合并批处理。

React 18 则无论是在普通的事件处理程序中还是异步的事件处理程序中, 都会进行批处理。举个例子如下:

注意:index.js 中使用的 Render 模式

以上例子中采用的是React 18 版本,在异步事件处理程序进行了两个setState。 可以看到每次 log 都会打印一次。说明在React 18中异步函数(定时器, Promise 等)中的setState 会执行合并批处理。

但是,有些情况下, 我不想要批处理, 那该怎么解决的呢?!!!!!!!

flushSync

官方提供了一个 API flushSync 来用于退出批处理。flushSync 会以函数为作用域,函数内部的多个setState仍然为批更新,这样就可以很精确的控制哪些不需要批量的更新。

注意:index.js 中使用的 Render 模式

以上例子中采用的是React 18 版本,在异步事件处理程序进行了两个setState。但是其中一个使用flushSync 来退出了批更新。结果可以看到每次 log 都会打印两次。

Transitons

产生的原因

Transitons 的直接翻译是“过渡”,本质上是为了解决渲染并发问题所提出。在React中一旦组件状态改变并触发了重新渲染,则无法停止渲染。直到组件中心渲染完毕,页面才能继续响应用户的交互。

为此React 18中提出一个新概念叫过渡,用于区分紧急和非紧急更新。

  • 紧急更新:表示反映了直接交互,如:键入,单击,按下等等。
  • 过渡(非紧急更新):表示UI 从一个视图转换到另外一个视图。

像一些行为,如 打字,点击或者按下等紧急更新需要立即响应, 以符合我们对物理对行为方式的直觉。否则用户就会觉得“不对劲”。但是过渡是不一样的,因为用户不希望在屏幕上看到每个中间值。

看如下的例子: 当滑块滑动的时候,下方的图表会一起更新,然后图表更新是一个cpu密集型操作,比较耗时,由于阻塞了渲染导致页面失去响应,用户就会非常明显的感受到卡顿。

实际上在这个例子中, 我们滑动滑块的时候,需要做两次更新。一个是滑块的条的更新 ; 另外一个是图表的更新

1
2
3
4
5
// 滑块的条的更新
setSliderValue(input);

// 图表的更新
setGraphValue(input);

在这里代码中, 用户期望第一个更新是即时的, 因为本地浏览器处理这些交互的速度很快,但是第二个更新可能会有点延迟,但是用户也并不期望它是立即完成, 因为用户还有很多工作要做(比如继续输入字符等)、实际上开发人员经常使用 debounce(防抖)等技术人为的延迟此类更新。

React 18 之前,所有的更新都是紧急更新, 这意味着上面这两种这两种状态会被同时渲染,并在渲染完成之前阻塞用户看到的交互的反馈结果。 因此我们缺少了一种方法来告诉React 哪些是紧急更新, 哪些是非紧急更新。

startTransition

React 提供 startTransition API 就可以解决如上问题,可以通过startTransition来包装非紧急更新,如果出现更紧急的更新(如点击,键入)等则会中断。

  • startTransition接受一个回调函数,用于告知React需要延迟更新的state.
  • 如果某个state的更新会导致组件挂起,则应该包裹startTransition中。
1
2
3
4
5
6
7
8
9
10
11
import { startTransition } from 'react';

// 滑块的条的更新 (未被标记则马上执行)
setSliderValue(input);

// 被startTransiton标记后为过渡更新
startTransition( () => {
// 非紧急更新,会被降低优先级,延迟执行
setGraphValue(input);
});

使用后可以明显的感受到,虽然图表的更新还是会有些延迟,但是整体的用户体验相对之前是非常好的。

开发中开发者可以通过startTransition hook 来决定哪些更新被标记为transition。一旦被标记则代表低优先级执行。即React 知道哪些state可以延迟更新。通过区分更新优先级,让更高级的时间保持响应,提高用户交互体验,保持页面响应。

startTransition 中的更新被视为非紧急更新,如果出现更紧急的更新(如点击或按键),更新则会被中断,如果用户中断了startTransition 中的更新(例如连续输入多个字符)、React将丢掉未完成的过时的渲染工作,只渲染最新的更新。

再来看另外一个例子:输入字符串后展示结果得场景模拟,通过伪造大量搜索结果,模拟容易卡顿的情况。

在这个例子中, 我们输入字符串之后,对输入字符串进行 20000 个节点的渲染。在没有使用startTransition的情况下, 我们输入一个字符串之后,result值马上进行响应,列表渲染立即开始,造成卡顿,输入框停止了对用户输入的响应。知道渲染结束,输入框才会继续响应。

同样的例子我们使用startTransition

很明显,输入字符串之后,result值响应被延后,保证了页面的反馈,知道输入结束, 才开始响应result,渲染列表

startTransition 与 setTimeout 的区别

一个重要的区别是startTransition 不像 setTimeout那样稍后执行,它是立即执行。传递给startTransition 的函数同步运行,但是函数内部的任何更新都标记为”transitions“(过渡更新)。React 将在稍后处理更新时使用该信息来决定如何渲染更新。这表示我们开始更新的时间比在定时器中包裹的更新时间要早,在一个网络较快的设备商,两次更新之间的延迟非常小。在一个网速较慢的设备商,延迟会更大,但是UI会保持响应。

另外一个重要的区别是 setTimeout内存在紧急更新时仍然会锁定页面。当定时器触发时,如果用户仍然在输入或与页面交互,它们仍然阻止与页面交互。但是用startTransition标记的状态更新是可以中断的,所以它们不会锁定页面。

useTransition

1
2
3
4
5
6
7

import { useTransition } from 'react'

const [isPending, startTransition] = useTransition({timeoutMs: 2000})
// 例如, 在pending状态下,您可以展示一个Spinner
{ isPending ? < Spinner /> : null }

  • useTransiton 接受一个回调函数,用来告诉React需要延迟更新的state
  • isPeding 是一个布尔值,这是React告知我们是过渡完成
  • useTransition 接受带有timeoutMs的延迟响应值,,如果给定的timeoutMs内未完成, 它会强制执行startTransition函数内的state的更新。

useDeferredValue

useDeferredValue返回一个延迟响应的值,可以让一个state延迟生效,只有当前没有紧急更新时, 该值才会变成最新值。
useDeferredValueuseTransition都是标记了一次非紧急更新。

1
2
3
4
5

import { useDeferredValue } from 'react';

const deferredValue = useDeferredValue(value);

useDeferredValueuseTransition的异同:

  • 相同点: useDeferredValueuseTransition 本质上是一样的, 都是标记成非紧急更新任务。
  • 不同点:useTransition是把更新任务变成了延迟任务,useDeferredValue是产生一个新值,这个值作为延迟状态。

useDeferredValuedebounce ,setTimeout的区别:
debouncesetTimeout都会有一个固定的延迟,而useDeferredValue的值只会在渲染耗费的时间后更新。在性能好的机器上延迟会变小,反之会变长。

useId

useId 是一个新的hook,用于在客户端上和服务器上生成唯一ID, 同时避免hydration mismatches

简单介绍下SSR的流程:
在服务端,我们将React组件渲染成一个字符串,这个过程叫脱水(dehydrate),字符串以HTML的形式传送给客户端作为首屏的内容、到了客户端之后React需要对该组件重新激活,用于参与新的渲染更新等过程, 这个过程叫注水hydrate

当我们使用React 进行服务端渲染时会遇到一个问题: 如果当前的组件已经在服务端渲染过了,但是在客户端并没有什么手段知道这个事情,于是客户端会再在渲染一次,这样就造成了冗余的渲染。

因此,React 18提出了useId这个hook来解决这个问题。它使用的是树状的结构(在客户端和服务端绝对稳定)来生成id

useSyncExternalStore

useSyncExternalStore 主要是用来解决外部数据tearing(撕裂)问题, 是由 useMutableSource改变而来。

撕裂:在屏幕上看到了一个用一个物体的不同帧的影像,画面仿佛是”撕裂的”。对应React中,指使用了过去版本的状态进行画面渲染引起的UI不一致或者崩溃。

撕裂现象:

引入并发渲染后, 渲染十四肯呢个被更高优先级的任务中断, 这也使得tearing成为可能。但事实React 本身对弈state的更新做了很多工作来避免这个问题,但是如果我们依赖了外部的状态,比如Redux, 它在控制状态的时并非直接使用Reactstate, 而是在外部值是维护了一个store对象,脱离了React的管理,也就无法依靠React自定解决撕裂问题,因此React提供了这样的一个API

同样的如上例子,只不过我们使用useSyncExternalStore创建state:

1
2
3
4
5
6
7
8

const useSelectorByNewAPI = (store, selector) => {
// 使用 useSyncExternalStore 防止撕裂
return useSyncExternalStore(
store.subscribe,
useCallback(() => selector(store.getState()), [store, selector])
);
};

获取值的时候:

1
2
3
4
5
// 使用 useSyncExternalStore 防止撕裂
const value = useSelectorByNewAPI(
store,
useCallback((state) => state.text, [])
);

具体的结果:

参考文章:

  1. 一文让你搞懂React18新特性及其实现
  2. React 18 startTransition
文章作者: 舒小琦
文章链接: https://shuliqi.github.io/2022/07/11/React-18-新特性/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 舒小琦的Blog