专栏名称: 程序员大咖
程序员大咖,努力成就期待着的自己。分享程序员技术文章、程序员工具资源、程序员精选课程、程序员视频教程、程序员热点资讯、程序员学习资料等。
目录
相关文章推荐
Foodaily每日食品  ·  配角掀桌,从默默无闻到霸占C位:卖疯了的坚果 ... ·  2 天前  
艾格吃饱了  ·  有时候觉得上班也挺好的。。。 ·  4 天前  
每天学点做饭技巧  ·  可媲美大牌的平价冲牙器,360度全方位清洁口 ... ·  4 天前  
51好读  ›  专栏  ›  程序员大咖

React 并发 API 实战,这几个例子看懂你就明白了

程序员大咖  · 公众号  ·  · 2025-01-15 10:24

正文

什么是并发

并发是一种执行模型,它允许程序的不同部分可以不按顺序执行,而不影响最终结果。你可能听说过多线程或多进程。由于浏览器中的 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






请到「今天看啥」查看全文