概述
想必我们在项目中可能使用过Suspense
组件,这个组件当其子组件数据未准备好之前会显示占位符内容(后备方案),更加注重用户体验,一般场景就是当我们使用懒加载、异步请求数据会通过该组件添加loading
效果。而useDeferredValue
则是更加关注渲染性能,在数据未加载之前会返回旧值,并结合memo
来减少不必要的渲染,如果不使用memo
则优化无效。本文继续通过基本使用和源码来深入学习useDeferredValue
的原理
基本使用
useDeferredValue
是一个 React Hook,可以让你延迟更新 UI 的某些部分。接收两个参数,并返回旧值:
value
: 想延迟的值,可以是任何类型。initialValue
:仅 Canary 可选 initialValue :在组件初始渲染期间使用的值。如果省略此选项, useDeferredValue 则不会在初始渲染期间延迟,因为它没有可以渲染的 value 先前版本。currentValue
(返回值):在初始渲染期间,返回的延迟值将与您提供的值相同。在更新过程中,React 将首先尝试使用旧值重新渲染(因此它将返回旧值),然后在后台尝试使用新值进行另一次重新渲染(因此它将返回更新的值)。
React版本说明:
- Canary:频繁发布,实验性功能,快速迭代和反馈,不推荐用于生产环境。
- Alpha:早期测试版本,新功能和改进,功能不够稳定。
- Beta:功能基本完成,有一些 bug,用于广泛测试和反馈。
- RC:功能完成,经过广泛测试,主要用于发现和修复小问题,接近最终版本。
- Stable:最终发布的稳定版本,适用于生产环境,功能稳定可靠
进度链可以这样理解: Canary - Alpha - Beta - RC - Stable(生产环境)
我们都是基于生产环境介绍,所以会省略initialValue参数的说明。
官网Demo:
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
在代码中我们使用到了Suspense
和SearchResults
,在上面的示例中,输入 “a”,等待结果加载完成,然后将输入框编辑为 “ab”。输入框input
值会立即更新,但是SearchResults
组件的内容会延迟更新。注意,现在你看到的不是 suspense 后备方案,而是旧的结果列表,直到新的结果加载完成。这是因为suspense是在数据未加载之前显示后备方案,而useDeferredValue在数据未加载之前会返回旧值并且降低更新优先级,此时在suspense看来值已经加载完了只不过是旧值,所以不会显示后备方案。
两者区别如下:
Suspense 注重用户体验,处理异步数据或组件加载时的加载状态,提供优雅的占位符
。useDeferredValue 注重渲染性能,通过延迟低优先级更新保持界面的高响应性,避免界面卡顿。
需要注意的是:
Suspense
和useDeferredValue
同时使用时,useDeferredValue
会导致Suspense
后备方案失效useDeferredValue
传递给组件时,必须要memo
包裹,否则优化失效
源码解析
Mount首次渲染
在首次渲染时,useDeferredValue
会接受一个初始值,并返回该初始值作为延迟值(因为首次渲染,没有旧值,而initialValue
仅在Canary版本有效)
function mountDeferredValue<T>(value: T, initialValue?: T): T {
// 初始化创建fiber.hook
const hook = mountWorkInProgressHook();
return mountDeferredValueImpl(hook, value, initialValue);
}
//
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
// 省略了设置initialValue时的情况
hook.memoizedState = value;
return value;
}
看代码也很简单,就是在Mount时将value缓存之后就直接返回了,也和上面所说的当初次渲染不设置initialValue
时,不会进行延迟更新所符合。
Update更新时
上面说到,当我们更新状态时,该Hook会在数据未返回之前仍然返回旧值 hook.memoizedState
,下面来看源码是如何处理的
function updateDeferredValue<T>(value: T, initialValue?: T): T {
const hook = updateWorkInProgressHook();
const resolvedCurrentHook: Hook = (currentHook: any);
const prevValue: T = resolvedCurrentHook.memoizedState;
return updateDeferredValueImpl(hook, prevValue, value, initialValue);
}
- updateWorkInProgressHook会复用现有的Hook链表将本次更新任务添加到hook.updateQueue中
- 获取缓存值,用于对比是否更新
- 调用
updateDeferredValueImpl
函数并返回旧值
function updateDeferredValueImpl<T>(
hook: Hook,
prevValue: T,
value: T,
initialValue?: T
): T {
if (is(value, prevValue)) {
return value;
} else {
if (isCurrentTreeHidden()) {
const resultValue = mountDeferredValueImpl(hook, value, initialValue);
if (!is(resultValue, prevValue)) {
markWorkInProgressReceivedUpdate();
}
return resultValue;
}
const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
if (shouldDeferValue) {
const deferredLane = requestDeferredLane();
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
deferredLane
);
markSkippedUpdateLanes(deferredLane);
return prevValue;
} else {
markWorkInProgressReceivedUpdate();
hook.memoizedState = value;
return value;
}
}
}
updateDeferredValueImpl
函数主要是降低更新优先级以达到推迟更新的作用,因为调度器会按照优先级高低调度任务。主要分为几部分,我们拆开代码来看:
if (isCurrentTreeHidden()) {
// Revealing a prerendered tree is considered the same as mounting new
// one, so we reuse the "mount" path in this case.
const resultValue = mountDeferredValueImpl(hook, value, initialValue);
// Unlike during an actual mount, we need to mark this as an update if
// the value changed.
if (!is(resultValue, prevValue)) {
markWorkInProgressReceivedUpdate();
}
return resultValue;
}
通过isCurrentTreeHidden
来判断当前是否在隐藏树中,一般是Suspense
组件包裹。如果在组件中没有使用Suspense
组件,则会跳过这段代码。
隐藏树(Hidden Tree)是 React 的一种机制,用于处理组件的懒加载和异步数据加载等场景,确保在数据准备好之前不会渲染不完整的内容。当组件的数据依赖于异步操作(如网络请求)时,React 可能会将当前树标记为“隐藏”,以确保异步操作完成后再进行更新。
在这段代码中表示,如果当前更新发生子隐藏树中时,由于React具有一致性,所以会复用挂载,即执行mountDeferredValueImpl
函数重新挂载,将newValue
缓存在fiber.memoizedState
中,然后判断新旧值是否变化,如果变化会调用markWorkInProgressReceivedUpdate
添加更新标记,表示当前fiber需要更新。
这里先通过
is(value, prevValue)
判断值是否变化,以此来减少不必要的更新,然后再通过!is(resultValue, prevValue)
判断是否给当前fiber标记更新是因为mountDeferredValueImpl
返回的resultValue
可能是initialValue
并不是value
,上面代码省略了对于initialValue的处理,所以可能会有这个疑问,在这里解释下。
这段代码也能看出会优先执行useDeferredValue
, 间接解释了当Suspense
组件和useDeferredValue一同使用时,会覆盖其后备方案fallback
的执行。
当不存在隐藏树时(即isCurrentTreeHidden() === false
),会通过includesOnlyNonUrgentLanes
判断当前更新任务是否是紧急优先级:
export function includesOnlyNonUrgentLanes(lanes: Lanes): boolean {
// TODO: Should hydration lanes be included here? This function is only
// used in `updateDeferredValueImpl`.
const UrgentLanes = SyncLane | InputContinuousLane | DefaultLane;
return (lanes & UrgentLanes) === NoLanes;
}
从代码知道React定义同步、用户输入、默认(普通的更新)都是紧急更新,然后通过位运算,判断当前更新优先级是否是紧急任务。如果不是紧急任务,则shouldDeferValue === false
,不需要推迟更新,直接更新缓存值并返回新值,并调用markWorkInProgressReceivedUpdate
添加更新标记,等待调度更新。
这里对于优先级低的任务不需要推迟是因为在调度器中会根据优先级高低调度任务,本身低优先级的任务就会延迟执行,所以useDeferredValue主要针对的还是对于高优先级的渲染优化,避免卡顿阻塞交互。
如果高优先级则需要进行推迟更新,即shouldDeferValue === true
// 获取推迟的优先级
const deferredLane = requestDeferredLane();
// 将本次更新优先级和低优先级的推迟优先级合并,以此来降低本次任务的优先级
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
deferredLane
);
// 跳过本次更新
markSkippedUpdateLanes(deferredLane);
// 返回旧值
return prevValue;
从上面代码能看出来useDeferredValue
推迟更新的本质就是将高优先级的任务和低优先级的推迟优先级合并,以此来降低本次更新的优先级,在调度器中由延迟调用来达到延迟更新的目的。
Demo运行流程
上面介绍了useDeferredValue
的基本原理,接下来从一个demo流程来进一步加深对其的理解:
初始状态
query
初始值:''
deferredQuery
初始值:''
isCurrentTreeHidden()
:false
第一次输入流程
- 用户在输入框中输入一个新值(例如:
'a'
)。 setQuery('a')
被调用,触发App
组件的重新渲染。
组件重新渲染
query
值变为'a'
。- 调用
useDeferredValue(query)
。
useDeferredValue
执行
- 实际调用
updateDeferredValue
,添加更新任务hook.updateQueue
- 调用
updateDeferredValueImpl
。
updateDeferredValueImpl
执行
is(value, prevValue)
prevValue
为''
,value
为'a'
,两者不同。
isCurrentTreeHidden()
为false
,进入非隐藏树逻辑。- 调用
includesOnlyNonUrgentLanes(renderLanes)
检查优先级。- 此时是用户输入为紧急优先级,需要进行延迟更新
requestDeferredLane
获取推迟低优先级。mergeLanes
合并优先级markSkippedUpdateLanes()
跳过本次更新。- 返回旧值
''
。
Suspense 处理
SearchResults
组件接收到新的deferredQuery
值''
。Suspense
继续显示当前内容,不会显示fallback
,因为 deferredQuery 还是旧值。
第二次输入流程
- 用户在输入框中输入另一个新值(例如:
'ab'
)。 setQuery('ab')
被调用,触发App
组件的重新渲染。
组件重新渲染
query
值变为'ab'
。- 调用
useDeferredValue(query)
。
useDeferredValue
执行
- 实际调用
updateDeferredValue
,添加更新任务hook.updateQueue
- 调用
updateDeferredValueImpl
。
updateDeferredValueImpl
执行
is(value, prevValue)
prevValue
为 第一次返回的''
,value
为'ab'
,两者不同。
isCurrentTreeHidden()
为false
,进入非隐藏树逻辑。- 调用
includesOnlyNonUrgentLanes(renderLanes)
检查优先级。- 此时是用户输入为紧急优先级,需要进行延迟更新
requestDeferredLane
获取推迟低优先级。mergeLanes
合并优先级markSkippedUpdateLanes()
跳过本次更新。- 返回旧值
''
。
Suspense 处理
SearchResults
组件接收到新的deferredQuery
值''
。Suspense
继续显示当前内容,不会显示fallback
,因为 deferredQuery 还是旧值。
这也解释了运行上面Demo时,当一直持续输入时,并不会显示Suspense
的后备方案,而是显示的''
,当我们停止输入请求回来之后才会显示最新的结果。
总结
通过以上流程项目我们对useDeferredValue
这个Hook已经非常了解了,这里就简单总结下:这个Hook主要针对高优先级任务进行延迟更新,而之所以能延迟更新是因为通过合并推迟优先级来降低了该更新任务优先级,依赖于调度器根据优先级调度任务的原理
。
题外话 - 号外
本文也是根据这些文章学习进行梳理在自己理解的基础上书写的,如有问题,还请指正。
有兴趣的朋友可以关注一下公众号,方便随时随地一起交流学习。