并发性是我们在 React 18 发布后获得的重大成就之一。由于此功能是完全选择加入的,并且 React 18 向后兼容以前的版本,因此您甚至可能没有注意到新功能。那么并发性是什么、它是如何工作的以及它如何改进您的应用程序的呢?
什么是并发
并发是一种执行模型,其中程序的不同部分可以无序执行,而不会影响最终结果。您可能听说过不同风格的并发性多线程或多处理。由于浏览器中的 JavaScript 只能访问一个线程(worker 在单独的线程中运行,但它们与 React 并不真正相关),因此我们不能使用多线程来并行我们的一些计算。为了确保资源的最佳使用和页面的响应能力,JS 必须采用不同的并发模型:协作式多任务处理。这听起来可能过于复杂,但不要害怕,您已经熟悉这个模型并且肯定已经使用过它。
我说的是 -s 和 /。协作式多任务处理意味着程序的编写方式使某些部分在等待某些事件时可能会暂停,并在稍后恢复。例如,要等待对 you use 的响应。这告诉浏览器,它可以在响应不存在时暂停当前函数并执行一些其他工作。当响应到达时,浏览器将等待活动函数产生 (using ),然后恢复我们之前的函数,该函数现在可以处理 HTTP 响应。
diff
+ Promise async await fetch await await
它与 React 的关系
在 React 18 之前,React 中的所有更新都是同步的。如果 React 开始处理更新,它无论如何都会完成它(当然,除非你关闭选项卡)。即使这意味着忽略此时发生的用户事件,或者如果您有特别重的组件,则冻结页面。对于较小的更新来说,这很好,但对于涉及渲染大量组件(如路由更改)的更新,它会对用户体验产生负面影响。
React 18 引入了两种类型的更新:紧急状态更新和过渡状态更新。默认情况下,所有状态更新都是紧急的,此类更新不能中断。转换是低优先级更新,可能会被中断。从现在开始,我还将使用"高优先级更新"和"低优先级更新"。
为了保持向后兼容性,默认情况下,React 18 的行为与以前的版本相同,所有更新都是高优先级的,因此是不可中断的。要选择并发呈现,您需要使用 或 将更新标记为低优先级。
diff
+ startTransition
+ useDeferredValue
中断和切换的工作原理
在渲染低优先级更新时,在渲染每个组件后,React 将暂停并检查是否有高优先级更新需要处理。如果有,React 将暂停当前渲染并切换到渲染高优先级更新。处理完后,React 将返回渲染低优先级更新(如果不再相关,则丢弃它)。除了高优先级更新外,React 还会检查当前渲染是否不需要太多时间。如果是这样,React 将返回给浏览器,因此它可以重新绘制屏幕以避免滞后和冻结。
由于 React 只能在渲染组件之间产生(它不能在组件中间停止),因此如果您有一两个繁重的组件,并发渲染将无济于事。如果组件渲染耗时 300 毫秒,则浏览器将被阻塞 300 毫秒。并发渲染真正闪耀的地方是,当您的组件只是稍微慢一点,但组件的数量如此之多,以至于它们加起来的总渲染时间相当长。
悬念呢?
您可能听说过 CPU 密集型程序。大多数情况下,此类程序会主动使用 CPU 来完成其工作。我们前面提到的慢速组件可以归类为受 CPU 限制的组件:为了更快地渲染,它们需要更多的资源。
与受 CPU 限制的程序相反,有受 IO 限制的程序。此类程序将大部分时间花在与输入输出设备(例如磁盘或网络)交互上。React 中负责处理 IO(主要以网络请求的形式出现)的组件是 Suspense。我介绍了它是如何工作的本指南,您可能想查看一下以更好地了解我们在说什么。
如果组件在低优先级更新期间挂起,则 Suspense 的行为略有不同。如果 Suspense 边界内已经有内容显示,React 不会像往常一样处理暂停并显示回退,而是会暂停渲染并切换到其他任务,直到 promise 解决,然后提交一个包含新内容的完整子树。这样,React 就可以避免隐藏已经存在的内容。如果组件在第一次渲染期间挂起,则会显示回退。
如何启动过渡
有几种方法可以启动转换,其中最基本的是功能。你像这样使用它:
diff
+ startTransition
javascript
import { startTransition, useState } from 'react';
const StartTransitionUsage = () => {
const onInputChange = (value: string) => {
setInputValue(value);
startTransition(() => {
setSearchQuery(value);
});
};
const [inputValue, setInputValue] = useState('');
const [searchQuery, setSearchQuery] = useState('');
return (<div>
<SectionHeader title="Movies" />
<input
placeholder="Search"
value={inputValue}
onChange={(e) => onInputChange(e.target.value)}
/>
<MoviesCatalog searchQuery={searchQuery} />
</div>);
};
这里发生的事情是,当用户输入搜索输入时,我们像往常一样更新状态变量,然后调用我们传递一个函数的位置,并进行另一个状态更新。这个函数会立即被调用,React 会记录在执行过程中所做的任何状态更改,并将它们标记为低优先级更新。请注意,只应将同步函数传递给(至少从 React 18.2 开始)。
diff
+ inputValue
+ startTransition
因此,在我们的示例中,我们实际上启动了两个更新:一个紧急(更新)和一个过渡(更新)。 组件可能会使用 Suspense 通过搜索查询获取电影,这将使该组件受 IO 限制。此外,它可以渲染相当长的电影卡列表,这可能也会使其受 CPU 限制。通过过渡,此组件不会在加载数据时触发悬念回退(将显示过时的 UI),也不会在渲染一长串卡片时冻结我们的浏览器。
diff
+ inputValue
+ searchQuery
+ MoviesCatalog
需要注意的是,对于受 CPU 限制的组件,它们应该用 包装,否则它们将在每次高优先级渲染时重新渲染,即使它们的道具没有改变,这将对应用程序的性能造成影响。
diff
+ React.memo
startTransition是最基本的功能,旨在在 React 组件之外使用。为了从 React 组件开始过渡,我们有一个更酷的版本:钩子。
diff
+ useTransition
javascript
import { useTransition, useState } from 'react';
const UseTransitionUsage = () => {
const onInputChange = (value: string) => {
setInputValue(value);
startTransition(() => {
setSearchQuery(value);
});
};
const [inputValue, setInputValue] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [isPending, startTransition] = useTransition();
return (<div>
<SectionHeader title="Movies" isLoading={isPending} />
<input
placeholder="Search"
value={inputValue}
onChange={(e) => onInputChange(e.target.value)}
/>
<MoviesCatalog searchQuery={searchQuery} />
</div>)
};
使用此钩子,您不会直接导入;相反,您可以调用钩子,它返回一个包含两个元素的数组:一个布尔值,指示是否有任何低优先级更新正在进行中(从此组件启动)和用于启动转换的函数。
diff
+ startTransition
+ useTransition()
+ startTransition
当你以这种方式启动过渡时,React 实际上会做 2 次渲染:一个是高优先级渲染,另一个是低优先级更新,其中包含您传递给的实际状态更改。因此,请谨慎使用。
diff
+ isPendingtrue
+ startTransition
+ React.memo
我们得到的另一个新钩子是,如果在关键组件和重型组件中使用相同的状态,则它非常有用。就像我们上面的例子一样。多么方便,是吧?这是你使用它的方式:
diff
+ useDeferredValue
javascript
import { useDeferredValue, useState } from 'react';
const UseDeferredValueUsage = () => {
const [inputValue, setInputValue] = useState('');
const searchQuery = useDeferredValue(inputValue);
return (<div>
<SectionHeader title="Movies" isLoading={inputValue !== searchQuery} />
<input
placeholder="Search"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<MoviesCatalog searchQuery={searchQuery} />
</div>);
};
在低优先级渲染和第一次高优先级渲染中,将存储传递的值并立即返回它,因此他们将是相同的字符串。但是在后续的高优先级渲染中,React 将始终返回存储的值。但它也会将你传递的值与存储的值进行比较,如果它们不同,React 将安排一个低优先级的更新。如果该值在高优先级更新期间再次更改,而低优先级更新正在进行中,React 将丢弃它并使用最新值安排新的低优先级更新。
diff
+ useDeferredValue
+ inputValue
+ searchQuery
使用此钩子,您可以拥有相同状态的两个版本:一个用于输入字段等关键组件(滞后通常是不可接受的),另一个用于搜索结果等组件(用户习惯于较长的延迟)。
结束语
并发肯定是一个有趣的功能,我相信一些复杂的应用程序会从中受益。更重要的是,它可能已经在你最喜欢的 React 框架的后台使用(Remix 和 Next 都使用它进行路由)。而且我怀疑,一旦 Suspense for Data Fetching 达到生产就绪状态,它将更加受欢迎。但就目前而言,您仍有时间学习并逐渐将其应用到您的应用程序中。如果你想深入了解 React 中的并发性,了解更多的细节和历史背景,请查看 Ivan Akulov 的这个演讲,这很好。
公众号:程序员白特