什么是并发
并发是一种执行模型,它允许程序的不同部分可以不按顺序执行,而不影响最终结果。你可能听说过多线程或多进程。由于浏览器中的 JavaScript 只能访问一个线程(虽然 Web Workers 在单独的线程中运行,但它们和 React 关系不大),我们不能使用多线程来并行处理一些计算。为了确保资源的最佳利用和页面的响应性,JavaScript 必须采用不同的并发模型:协作式多任务。这听起来可能有点复杂,但别担心,你已经熟悉这个模型了,而且肯定用过。
它和 React 有什么关系
在 React 18 之前,React 中的所有更新都是同步的。如果 React 开始处理一个更新,它会完成它,不管你在干嘛(当然,除非你关闭了标签页)。即使这意味着忽略了此时发生的用户事件,或者如果你有一些特别重的组件,页面会冻结。对于较小的更新来说,这还好,但对于涉及渲染大量组件的更新(比如路由变化),它对用户体验产生了负面影响。
React 18 引入了两种类型的更新:紧急状态更新和 transition 状态更新。默认情况下,所有状态更新都是紧急的,这样的更新不能被中断。transition 是低优先级的更新,可以被中断。从现在起,我也将使用“高优先级更新”和“低优先级更新”来指代它们。
为了保持向后兼容性,默认情况下,React 18 的行为和之前的版本一样,所有更新都是高优先级的,因此不可中断。要启用并发渲染,你需要通过使用
startTransition
或
useDeferredValue
将更新标记为低优先级。
中断和切换是如何工作的
在渲染低优先级更新时,React 在渲染完每个组件后会暂停,并检查是否有高优先级更新需要处理。如果有,React 会暂停当前渲染,切换到渲染高优先级更新。处理完这些后,React 会返回到渲染低优先级更新(或者如果它无效了,就丢弃它)。除了高优先级更新,React 还会检查当前渲染是否耗时过长。如果耗时过长,React 会将控制权还给浏览器,以便它可以重绘屏幕,避免卡顿和冻结。
由于 React 只能在组件之间暂停(它不能在组件中间停下来),所以如果你有一两个特别重的组件,并发渲染帮助不大。如果组件渲染需要 300 毫秒,浏览器就会被阻塞 300 毫秒。并发渲染真正发挥作用的地方是当你的组件只是稍微慢一点,但它们的数量比较多,以至于总渲染时间相当长。
那 Suspense 呢?
你可能听说过 CPU 密集型程序。这类程序大多数时间都在积极地使用 CPU 来完成它们的工作。我们之前提到的慢组件可以归类为 CPU 密集型:为了更快地渲染,它们需要更多的资源。
与 CPU 密集型程序相反,还有 I/O 密集型程序。这类程序大部分时间都在与输入输出设备(比如磁盘或网络)交互。在 React 中负责处理 I/O 的组件是 Suspense。
如果组件在低优先级更新期间暂停,Suspense 的行为会有所不同。如果 Suspense 边界内已经有内容显示,React 不会像通常那样处理暂停并显示 fallback 内容,而是会暂停渲染,转而处理其他任务,直到 Promise resolved,然后提交一个带有新内容的完整子树。这样,React 避免了隐藏已经显示的内容。如果组件在首次渲染期间暂停,将显示 fallback 内容。
如何启动 transition
启动 transition 有几种方法,最基本的是
startTransition
函数。你像这样使用它:
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
>
)
}
这里发生的事情是,当用户在搜索输入框中输入时,我们像往常一样更新状态变量
inputValue
,然后调用
startTransition
,传入一个包含另一个状态更新的函数。这个函数会立即被调用,React 会记录其执行期间所做的任何状态更改,并将它们标记为低优先级更新。请注意,至少在 React 18.2 中,只能传递同步函数给
startTransition
。
所以在我们的示例中,我们实际上启动了两个更新:一个是紧急的(更新
inputValue
),另一个是 transition(更新
searchQuery
)。
MoviesCatalog
组件可能会使用 Suspense 来根据搜索查询获取电影,这将使该组件成为 I/O 密集型。此外,它还可以渲染相当长的一系列电影卡片,这可能使它也成为 CPU 密集型。有了 transition,这个组件在加载数据时不会触发 Suspense fallback(会显示过时的 UI),在渲染长列表的电影卡片时也不会卡住浏览器。
需要注意的是,在 CPU 密集型组件的情况下,它们应该用
React.memo
包裹起来,否则即使它们的 props 没有变化,它们也会在每次高优先级渲染时重新渲染,这会影响你应用的性能。
startTransition
是最基础的函数,主要用于 React 组件之外。要从 React 组件内部启动 transition,我们有一个更酷的版本:
useTransition
hook。
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
>
)
}
有了这个 hook,你不需要直接导入
startTransition
;相反,你调用
useTransition()
hook,它会返回一个包含两个元素的数组:一个 boolean 值,表示是否有任何低优先级更新正在进行(从这个组件发起),以及你用来启动 transition 的
startTransition
函数。
当你以这种方式启动 transition 时,React 实际上会进行两次渲染:一次高优先级渲染,将
isPending
翻转为 true,以及一次低优先级更新,包含你传递给
startTransition
的实际状态更改。所以要小心,用
React.memo
包裹“昂贵”的组件。
我们还有另一个新 hook 是
useDeferredValue
。如果相同的状态在关键和重型组件中都使用,它就变得有用了。就像我们上面的例子一样。多方便啊?这是你如何使用它:
import
{ useDeferredValue, useState }
from
'react'
const