并发模式(Concurrent Mode)1的一个关键特性是渲染可中断。
React 18 之前,更新内容渲染的方式是通过一个单一的且不可中断的同步事务进行处理。同步渲染意味着,一旦开始渲染就无法中断,直到用户可以在屏幕上看到渲染结果。
在并发渲染中,React 可以开始渲染一个更新,然后中途挂起,稍后又继续;甚至可能完全放弃一个正在进行的渲染。整个过程 UI 会保持一致。为了实现这一点,它会在整个 DOM 树被计算完毕前一直等待,完毕后再执行 DOM 变更。这样做,React 就可以在后台提前准备新的屏幕内容,而不阻塞主线程。这意味着用户输入可以被立即响应,即使存在大量渲染任务,也能有流畅的用户体验。
通过 time slice 将任务拆分为多个,然后 React 根据优先级来完成调度策略,将低优先级的任务先挂起,将高优先级的任务分配到浏览器主线程的一帧的空闲时间中去执行,如果浏览器在当前一帧中还有剩余的空闲时间,那么 React 就会利用空闲时间来执行剩下的低优先级的任务。
React 18 之后,可以立即开始使用并发模式的功能。如,可以使用 useTransition
在屏幕内容之间进行导航,而不会阻塞用户输入;或者使用 useDeferredValue
来节流处理开销巨大的重新渲染。
useTransition
用于将某些状态更新标记为非阻塞的 transition,以保持用户界面的响应性,特别是在处理耗时的状态更新时。
const [isPending, startTransition] = useTransition()
过渡(transition)更新是 React 中一个新的概念,用于区分紧急和非紧急的更新。
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”选项卡将会立即显示。
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,可以明显察觉到,滑块一直保持响应,而“树”直接渲染了最终结果。
useTransition
: 一个用于开启过渡更新的 Hook,组件或自定义 Hook 内部调用。startTransition
: 当 Hook 不能使用时,用于开启过渡的方法。传递给 Transition
的函数必须是同步的。React 会立即执行此函数,并将在其执行期间发生的所有状态更新标记为 transition。如果在其执行期间,尝试稍后执行状态更新(例如在一个定时器中执行状态更新),这些状态更新不会被标记为 transition。
标记为 transition 的状态更新将被其他状态更新打断。打断的内容被挂起,过渡机制会告诉 React 在后台渲染过渡内容时继续展示当前内容。
只有在可以访问该状态的 set
函数时,才能将其对应的状态更新包装为 transition。如果想启用 transition 以响应某个 prop 或自定义 Hook 值,需要使用 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
有两大优势:
如果要优化的工作不是在渲染期间发生的,那么防抖和节流仍然非常有用。例如,它们可以让你减少网络请求的次数。你也可以同时使用这些技术。而 useDeferredValue
更适合优化渲染,因为它与 React 自身深度集成,并且能够适应用户的设备。