本文将向你介绍React并发模式背后的理念,以及它的一些用法和好处。React的并发模式是一组创新的功能,旨在改善异步渲染的处理。这些改进使得终端用户的体验更好。
自古以来,困扰网络客户端的一个长期问题就是处理异步更新的渲染问题。React团队继续其在框架中引入雄心勃勃的解决方案的传统,在React 16.x发布线中加入了并发模式支持。
在很多情况下,对变化的状态进行天真的渲染会导致不太理想的行为,包括乏味的加载屏幕、不流畅的输入处理和不必要的旋转器,等等。
零敲碎打地解决这些问题容易出错且不一致。React的并发模式代表了一种全盘的、烘烤到框架中的解决方案。其核心思想是。React现在在内存中并发地绘制更新,支持可中断的渲染,并为应用程序代码提供与该支持互动的方法。
在React中启用并发模式
利用这些功能的API仍在变化中,你必须明确地安装它,像这样:
npm install react@experimental react-dom@experimental
并发模式是对React工作方式的一个全局性改变,它要求根级节点通过并发引擎。这是通过在应用根上调用createRoot来实现的,而不是只调用reactDOM.render() 。这在清单1中可以看到。
清单1.使用并发渲染器
ReactDOM.createRoot(
document.getElementById('root')
).render(<App />);
请注意,只有当你安装了实验包后,createRoot 才可用。而且由于它是一个根本性的变化,现有的代码库和库很可能与它不兼容。尤其是现在预置了UNSAFE_ 的生命周期方法是不兼容的。
因为这个事实,React在我们今天使用的老式渲染引擎和并发模式之间引入了一个中间步骤。这个步骤被称为 "阻塞模式",它更向后兼容,但并发功能较少。
从长期来看,并发模式将成为默认模式。在中期,React将支持以下三种模式,如本文所述。
- 遗留模式:
ReactDOM.render(<App />, rootNode)。现有的遗留模式。 - 阻断模式:
ReactDOM.createBlockingRoot(rootNode).render(<App />)。较少的突破性变化,较少的功能。 - 并发模式:
ReactDOM.createRoot(rootNode).render(<App />)。完全的并发模式,有许多突破性的变化。
React的一个新的渲染模式
并发模式从根本上改变了React渲染界面的方式,允许在获取数据的过程中渲染界面。这意味着React必须知道一些关于你的组件的信息。具体来说,React现在必须知道你的组件的数据获取状态。
React的新Suspense组件
最突出的功能是新的Suspense 组件。你用这个组件来通知React,UI的一个特定区域依赖于异步数据加载,并给它这种加载的状态。
这种能力在框架层面上发挥作用,意味着你的数据获取库必须通过实现Suspense API来提醒React其状态。目前,Relay为GraphQL做了这件事,而react-suspense-fetch项目正在处理REST数据的获取。
重申一下,你现在需要使用一个更智能的数据获取库,它能够告诉React它的状态是什么,从而允许React优化你的UI渲染的方式。
看一下React例子中的这个例子。清单2有重要的视图模板细节。
清单2.在视图中使用悬念
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
请注意,Suspense ,允许定义交替加载的内容。这类似于在旧的渲染引擎中,你可以根据组件的加载状态在组件内部使用不同的返回值,来渲染一个占位符,直到数据准备好。
在这个视图模板所使用的组件中,不需要特别的代码来处理加载状态。现在这一切都由框架和数据获取库在幕后处理。
例如,ProfileDetails 组件可以无辜地加载其数据并返回其标记,如清单3所示。同样,这取决于数据存储(在清单3中是resource 对象)是否实现了Suspense API。
清单3.简介细节
function ProfileDetails() {
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
数据获取的并发性
这个设置的一个重要好处是,所有的数据获取都是并发进行的,这一点值得重复。因此,你的用户界面既能从改进的渲染生命周期中获益,又能以简单和自动的方式实现多个组件的并行数据获取。
React的useTransition钩子
在你新的并发React工具包中的下一个主要工具是useTransition 钩子。这是一个更加细化的工具,允许你调整UI转换的发生方式。清单4有一个用useTransition 包裹过渡的例子。
清单4.useTransition
function App() {
const [resource, setResource] = useState(initialResource);
const [ startTransition, isPending ] = useTransition({ timeoutMs: 3000 });
return (
<>
<button
onClick={() => {
startTransition(() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
});
}}> Next </button>
{isPending ? " Loading..." : null}
<ProfilePage resource={resource} />
</>
);
}
清单4中的代码说的是:"延迟显示新的状态,最多三秒"。这段代码之所以有效,是因为ProfilePage ,当使用时,被一个Suspense 组件包裹着。React开始获取数据,而不是显示占位符,而是显示现有的内容,时间与定义的timeoutMs 。一旦获取完成,React将显示更新的内容。这是一个简单的机制,可以提高转换的感知性能。
useTransition 所暴露的startTransition 函数允许你包裹代码的获取部分,而isPending 函数暴露了一个布尔标志,你可以用来处理有条件的加载显示。
所有这些魔法都是可能的,因为React的并发模式已经实现了一种背景渲染机制。React在后台渲染你的更新状态的UI,而获取正在发生。你可以在这里更详细地了解它的工作原理。
React的useDeferredValue钩子
我们的最后一个例子涉及修复打字导致数据加载时的不稳定问题。这是一个相当典型的问题,通常用输入的减震/节流来解决。并发模式提供了一个更一致、更平滑的解决方案:useDeferredValue 钩子。
这里有一个例子。这个解决方案的天才之处在于,它让你得到了两个世界的最好结果。输入仍然是有反应的,而且一旦数据可用,列表就会更新。
清单5:useDeferredValue的作用
const [text, setText] = useState("hello");
const deferredText = useDeferredValue(text, { timeoutMs: 5000 });
// ....
<MySlowList text={deferredText} />
类似于我们用useTransition 来包装一个过渡,我们用useDeferredValue 来包装一个资源值。这使得该值可以像timeoutMs 值一样保持原状。所有管理这种改进的渲染的复杂性都由React和数据存储在幕后处理。
竞争条件的解决方案
使用Suspense和并发模式的另一个好处是,在生命周期钩子和方法中手动加载数据所引入的竞赛条件被避免了。数据被保证按照请求的顺序到达和应用。(这与Redux修复竞赛条件的方式类似。)因此,新模式避免了由于请求响应的交错而需要手动检查数据的滞后性。
这些是新并发模式的一些亮点。它们提供了引人注目的好处,将成为未来的规范。