写在前面:并发
并发模式(Concurrent Mode)1的一个关键特性是渲染可中断。
React 18 之前,更新内容渲染的方式是通过一个单一的且不可中断的同步事务进行处理。同步渲染意味着,一旦开始渲染就无法中断,直到用户可以在屏幕上看到渲染结果。
在并发渲染中,React 可以开始渲染一个更新,然后中途挂起,稍后又继续;甚至可能完全放弃一个正在进行的渲染。整个过程 UI 会保持一致。为了实现这一点,它会在整个 DOM 树被计算完毕前一直等待,完毕后再执行 DOM 变更。这样做,React 就可以在后台提前准备新的屏幕内容,而不阻塞主线程。这意味着用户输入可以被立即响应,即使存在大量渲染任务,也能有流畅的用户体验。
通过 time slice 将任务拆分为多个,然后 React 根据优先级来完成调度策略,将低优先级的任务先挂起,将高优先级的任务分配到浏览器主线程的一帧的空闲时间中去执行,如果浏览器在当前一帧中还有剩余的空闲时间,那么 React 就会利用空闲时间来执行剩下的低优先级的任务。
React 18 之后,可以立即开始使用并发模式的功能。如,可以使用 useTransition
在屏幕内容之间进行导航,而不会阻塞用户输入;或者使用 useDeferredValue
来节流处理开销巨大的重新渲染。
- useTransition:用于标记状态更新为非阻塞,保持 UI 响应性,适合处理耗时操作导致的状态变化;
- useDeferredValue:主要用于延迟渲染以提升性能和用户体验,特别是在快速变化的输入或数据加载过程中。
useTransition/startTransition
useTransition
用于将某些状态更新标记为非阻塞的 transition,以保持用户界面的响应性,特别是在处理耗时的状态更新时。
const [isPending, startTransition] = useTransition()
过渡(transition)更新是 React 中一个新的概念,用于区分紧急和非紧急的更新。
- 紧急更新 对应直接的交互,如输入,点击,按压等。需要立即响应的行为,如果不立即响应会给人卡顿的感觉。
- 过渡更新 将 UI 从一个视图过渡到另一个。不需要即时响应,有些延迟是可以接受的。
import { startTransition } from 'react';
// 紧急更新: 显示输入的内容
setInputValue(input);
// 将任何内部的状态更新都标记为过渡更新
startTransition(() => {
// 过渡更新: 展示结果
setSearchQuery(input);
});
如果一个过渡更新被用户中断(比如,快速输入多个字符),React 将会抛弃未完成的渲染结果,然后仅渲染最新的内容。
通过 transition,UI 仍将在重新渲染过程中保持响应性。
👀 官方示例:
用户点击“Posts”,然后立即点击“Contact”。
🌾 未使用 transition
⚠️ 应用程序在渲染减速选项卡时会冻结,UI 将变得无响应。Posts渲染完后,Contact 才渲染!
🌴 使用 transition
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
⚠️ 这会中断“Posts”的缓慢渲染,而“Contact”选项卡将会立即显示。
👀 经典案例-毕哥达拉斯树2:
const [isLeaning, startLeaning] = useTransition()
function changeTreeLean(event) {
const value = Number(event.target.value);
setTreeLeanInput(value); // update 滑块
// 开启 transition
if (enableStartTransition) {
startLeaning(() => {
setTreeLean(value);
});
} else {
setTreeLean(value);
}
}
🌾 全部为紧急更新:
通过下述 gif,可以明显察觉到,滑块到右侧已经卡住了。
🌴头部滑块为紧急更新,树为非紧急更新:
通过下述 gif,可以明显察觉到,滑块一直保持响应,而“树”直接渲染了最终结果。
开启 transition 有两种方式:
useTransition
: 一个用于开启过渡更新的 Hook,组件或自定义 Hook 内部调用。startTransition
: 当 Hook 不能使用时,用于开启过渡的方法。
传递给 Transition
的函数必须是同步的。React 会立即执行此函数,并将在其执行期间发生的所有状态更新标记为 transition。如果在其执行期间,尝试稍后执行状态更新(例如在一个定时器中执行状态更新),这些状态更新不会被标记为 transition。
标记为 transition 的状态更新将被其他状态更新打断。打断的内容被挂起,过渡机制会告诉 React 在后台渲染过渡内容时继续展示当前内容。
只有在可以访问该状态的 set
函数时,才能将其对应的状态更新包装为 transition。如果想启用 transition 以响应某个 prop 或自定义 Hook 值,需要使用 useDeferredValue
。
useDeferredValue
useDeferredValue
用于延迟更新 UI 的某些部分,以便在新内容加载期间显示旧内容,或者在用户输入快速时,避免界面频繁刷新导致的卡顿。
一旦 React 完成原始的重新渲染,它会立即开始使用新的延迟值处理后台重新渲染。由事件(例如输入)引起的任何更新都会中断后台重新渲染,并被优先处理。
const deferredValue = useDeferredValue(value) // value可以是任何类型
⚠️ 向
useDeferredValue
传递原始值(如字符串和数字)或在渲染之外创建的对象。如果在渲染期间创建了一个新对象,并立即将其传递给useDeferredValue
,那么每次渲染时这个对象都会不同(使用Object.is
进行比较),这将导致后台不必要的重新渲染。a1 = {a: 1} a2 = {a: 1} Object.is(a1, a2) // false
useDeferredValue
允许推迟渲染树的非紧急更新。这和防抖操作非常相似,但是有一些改进。它没有固定的延迟时间,React 会在第一次渲染在屏幕上出现后立即尝试延迟渲染。延迟渲染是可中断的,它不会阻塞用户输入。
- 当需要在用户输入时显示过时的数据,以避免界面闪烁或卡顿。
- 与
<Suspense>
集成,可以在数据加载期间显示旧内容而不是后备方案。
示例:
export default () => {
const [query, setQuery] = useState('');
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={query} />
</Suspense>
</>
);
}
尝试输入 "a"
,等待结果出现后,将其编辑为 "ab"
。此时 "a"
的结果会被加载中的后备方案替代。
使用 useDeferredValue
将延迟版本的查询参数向下传递。 延迟 更新结果列表,继续显示之前的结果,直到新的结果准备好。
export default function App() {
const deferredQuery = useDeferredValue(query);
return (
<>
{...}
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
输入 "a"
,等待结果加载完成,然后将输入框编辑为 "ab"
。注意,现在你看到的不是 suspense 后备方案,而是旧的结果列表,直到新的结果加载完成
防抖&节流
- 防抖 是指在用户停止输入一段时间(例如一秒钟)之后再更新列表。
- 节流 是指每隔一段时间(例如最多每秒一次)更新列表。
与防抖或节流, useDeferredValue
有两大优势:
-
不需要选择任何固定延迟时间。如果用户的设备很快(比如性能强劲的笔记本电脑),延迟的重渲染几乎会立即发生并且不会被察觉。如果用户的设备较慢,那么列表会相应地“滞后”于输入,滞后的程度与设备的速度有关。
-
执行的延迟重新渲染默认是可中断的。这意味着,如果 React 正在重新渲染一个大型列表,但用户进行了另一次键盘输入,React 会放弃该重新渲染,先处理键盘输入,然后再次开始在后台渲染。相比之下,防抖和节流仍会产生不顺畅的体验,因为它们是阻塞的:它们仅仅是将渲染阻塞键盘输入的时刻推迟了。
如果要优化的工作不是在渲染期间发生的,那么防抖和节流仍然非常有用。例如,它们可以让你减少网络请求的次数。你也可以同时使用这些技术。而 useDeferredValue
更适合优化渲染,因为它与 React 自身深度集成,并且能够适应用户的设备。
https://zh-hans.react.dev/blog/2022/03/29/react-v18#what-is-concurrent-react 什么是并发 React ↩︎
https://react-fractals-git-react-18-swizec.vercel.app/ react CM startTransition 示例 ↩︎