前言
2022 年 3 月 29 号,React 18
正式发布了,其实从 React 16
版本开始,React
团队已经开始普及“并发”的概念,在 v18
版本迭代的过程中,也有不少相关的并发特性知识在科普,那么v18
版本到底有哪些知识,这里就给大家介绍一下。
并发特性(Concurrent Features)
最早此概念受启发于 React Native
,因为 JavaScript
是单线程的,而本地平台则是运行在多个线程上的,由于为了良好的移动端的用户体验,React Native
就不得不考虑如何实现“并发调度”特性,这么好的特性如果运用在 Web
上,那么性能岂不会大大提升吗,于是 React
团队便开始结合此开始深入研究网络功能,比如“代码拆分”、“数据获取”、“流服务器渲染”、“服务器组件” 等,最终发现这些能力都是可构建的,因此就设计出了最初版的 Suspense API
,如我们最熟悉的 React.lazy + Suspense
实现组件懒加载。
⚠️ 注:目前
React 18
的新暂时未在React Native
内实现(技术上的限制)。
React 18 新功能
React 18 中的许多功能都建立在新的并发渲染器(Concurrent 模式)之上,React 在内部以优先队列和多重缓冲等复杂技术,来创建并行机制,让 React 得以实现新功能提升用户体验。
- 自动批处理能力;
- 支持 Suspense 的流式服务器端渲染;
- 新的严格模式(StrictMode);
- 新的 React Hook API(
useId
、useTransition
、useDeferredValue
); - 服务器组件(Server Components,开发中,不会包含在 React 18 稳定版中 =>
Next.js
、Hydrogen
已支持)
启用 Concurrent
模式
启用 Concurrent
模式比较简单,只需要改造一下 index.tsx
代码,改造结果如下:
1 | import React from 'react'; |
能力介绍
自动批处理
批处理是
React
将多个状态更新分组到一个重新渲染中以获得更好的性能。如果没有自动批处理,我们只能在React
事件处理程序中批处理更新。
React 18
版本之前,在合成事件之外的原生事件中(例如 setTimeout
、onclick
、Promise
) ,更新状态并不会进行批量处理(合并处理),这意味着在原生事件中多次调用状态更新会造成多次应用的重新渲染,往往我们只需要最后一次即可,在 React 18
版本后优化了这个问题。
1 | import React, { useState } from 'react'; |
- 17 版本(点击 4 次渲染了 8 次)
- 18 版本(点击 4 次渲染 4 次)
内容源自“React 18 之状态批处理”(见底部“参考文章”)
新的严格模式
StrictMode
是一个用来突出显示应用程序中潜在问题的工具。与Fragment
一样,StrictMode
不会渲染任何可见的UI
。它为其后代元素触发额外的检查和警告。
可以为应用程序的任何部分启用严格模式,如:
1 | import React from 'react'; |
StrictMode
目前有助于:
未来的 React 版本将添加更多额外功能
Suspense 新功能
在 React 18
中,服务器添加了对 Suspense
的支持,并使用并发渲染特性扩展了它的功能。如果组件树的一部分尚未准备好显示,Suspense
允许您以声明方式指定其加载状态,流程可如下所示:
新的客户端和服务器渲染 API
在上述 启用 Concurrent 能力
中已经介绍了客户端渲染写法,需要关注的是新 API 的到处地址均为 react-dom/client
。
由于暂未尝试服务端能力,详情可参考 React Dom 服务器文档。
新 Hook
useId
其作用就是用于在客户端和服务器上生成唯一 ID
。
在之前的版本中,我们可以使用 React
进行服务端渲染(SSR)。在开发模式上,我们可以在客户端与服务端共享同一个 React
组件。但是,这里就会有一个小问题:如果当前组件已经在服务端渲染过了,但是在客户端我们并没有什么手段知道这个事情,于是客户端还会重新再渲染一次,这样就造成了冗余的渲染。
要理解这个背景,我们需要对 SSR 的流程有一个简单的概念。
在服务端,我们会将 React 组件渲染成为一个字符串,这个过程叫做脱水「dehydrate
」。字符串以 html
的形式传送给客户端,作为首屏直出的内容。到了客户端之后,React 还需要对该组件重新激活,用于参与新的渲染更新等过程中,这个过程叫做注水「hydrate
」,那么这个过程中,同一个组件在服务端和客户端之间就需要有一个相对稳定的 id
来确定对应的匹配关系。
但是,React
在后续的更新中,就开始搞事情,客户端渲染有 reconciler
,服务端渲染有 fizz
,他们的作用大概相同,那就是根据某种优先级进行任务调度。于是,无论是客户端还是服务端,都可能不会按照稳定的顺序渲染组件了,递增的计数器方案就无法解决问题,而组件的树状结构即使渲染顺序不同,但整体的优先级顺序结构能保持稳定,可以用这种思路来解决重复渲染问题。
在同一个组件中,我们需要多个 id,那么一定不要重复的使用 useId
,而是基于一个 id 来创建不同的身份标识
1 | // 使用姿势 |
内容源自 “React 18 新特性之 useId 详细解读”(见底部参考文章)
useTransition
【使用场景】
异步加载数据的过渡状态管控(可以逐步淘汰 const [loading, setLoading] = useState(false)
,转而用 useTransition
来管控加载状态)
【使用方法】
1 | import React, { useState, useTransition, useCallback } from 'react'; |
【实际演练】
useDeferredValue
【使用场景】
大量数据在页面频繁重绘的情况,当然使用 lodash
的 debounce
也能实现类似的效果,只不过此 API
不用限制死时间,一旦当前的绘制完毕会自动进入下一次处理。
【使用方法】
⚠️ 注:使用方法比较简单,需要配合
Suspense
和useMemo
缓存(或手动对比判断)来得到中间加载状态,其中Suspense
主要用于初始化部分。
1 | import React, { useDeferredValue, useState, Suspense } from 'react'; |
【与 debounce 的区别】
debounce
有一个固定的人为延迟(如 80 个项,每项延迟 3ms),所以它总是会延迟,不论我们的电脑有多快。然而,useDeferredValue
的值只会在渲染耗费时间的情况下 “滞后”,React
并不会加入一点多余的延迟。在一个更实际的工作负荷,你可以预期这个滞后会根据用户的设备而不同。在较快的机器上,滞后会更少或者根本不存在,在较慢的机器上,它会变得更明显。但不论哪种情况,应用都会保持可响应。这就是此机制优于 debouncing
或 throttling
的地方,它们总是会引入最小延迟而且不可避免的会在渲染的时候阻塞进程。
【实际演练】
未来对服务器端的支持
原有能力
Suspense
+React.lazy
实现代码分割,方便打包工具打包;
现有能力
- 【服务端】流式渲染、唯一 ID(
useId
)、服务器组件(暂未推出,但next.js
等部分后端框架已实现); - 【客户端】异步加载(
useTransition
、useDeferredValue
);
未来能力
- 异步数据获取能力(不用手动处理
Loading
,全交给Suspense
);
1 | // 现有类似 ahook 的写法(自己处理状态) |
- 服务器组件推广,减少前端重复性组件成本;
参考文章
- concurrent 模式 API 参考(实验版)
- React 18 Note(视频可从 5:10 开始看)
- Streaming Server Rendering with Suspense(视频)
- Server-Components(react 团队 github 仓库)
- React 18 之状态批处理
- React 18 的新改进功能
- React 18 新特性之 useId 详细解读
- 给女朋友讲 React18 新特性:Automatic batching
useTranstion
和useDeferredValue
原理- React 18 Concurrent 之 useDeferredValue<译>
- 真实世界示例:为慢速渲染添加
startTransition
React 18
新特性useTransition
- React 18 Concurrent 之 startTransition<译>
- React 18 New Feature: startTransition
- React18 引入了新 Hook: useId ,用于生成唯一 id