一. Suspense
组件和use
方法介绍
use
是React
提供的API
,有两个作用,一个是获取Context
数据,等价于useContext
,另一个作用是接收promise
,常见场景是请求数据。需要注意的是如果接收promise
,需要配合Suspense
组件。
代码示例如下,执行流程首先是展示Loading
态,一秒之后展示hello world
function HelloWorld({ fetchData }) {
const data = use(fetchData)
return <h1>{data}</h1>
}
function App() {
const fetchData = new Promise(resolve => {
setTimeout(() => {
resolve('hello world')
}, 1000)
})
return (
<div>
<Suspense fallback={<h1>Loading....</h1>}>
<HelloWorld fetchData={fetchData} />
</Suspense>
</div>
)
}
Suspense
组件主要作用是包裹有异步操作子组件,常见场景有包裹使用use
子组件或懒加载组件,当异步子组件没有ready
时会先展示fallback
,ready
之后再展示子组件。
二. 实现use
获取
Context
数据原理参考文档手写React useContext,理解useContext原理,本文不再赘述。 本文主要解析promise
实例pending
和resolved
态的处理逻辑,rejected
态的处理逻辑读者自行了解。
use
方法逻辑比较简单,主要判断当前promise
实例状态,如果处于pending
态抛SuspenseException
异常,当promise
实例resolved
或rejected
,将对应的status
和value
记录到promise
实例上。
let suspendedThenable = null
function trackUsedThenable(thenable) {
switch (thenable.status) {
case 'fulfilled':
return thenable.value
case 'rejected':
throw thenable.reason
default:
if (typeof thenable.status === 'string') thenable.then(noop, noop)
else {
thenable.status = 'pending'
thenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
thenable.status = 'fulfilled'
thenable.value = fulfilledValue
}
},
error => {
if (thenable.status === 'pending') {
thenable.status = 'rejected'
thenable.reason = error
}
},
)
}
switch (thenable.status) {
case 'fulfilled':
return thenable.value
case 'rejected':
throw thenable.reason
}
// 记录promise实例
suspendedThenable = thenable
// 当promise处于pending状态时抛异常
throw SuspenseException
}
}
function useThenable(thenable) {
const result = trackUsedThenable(thenable)
return result
}
function use(usable) {
if (typeof usable === 'object' && usable !== null) {
// 判断是否是promsie实例
if (typeof usable.then === 'function') {
return useThenable(usable)
} else {
return readContext(usable)
}
}
}
2.1 异常处理
在往期文章手写mini React,理解React渲染原理中讲解了
React
渲染流程,但是没有处理异常逻辑,本文结合SuspenseException
解析React
渲染流程中异常处理逻辑。
renderRootSync
方法用于构建Fiber
树,这里主要关注handleThrow
方法,当组件调用use
方法并抛异常时,会被这个方法捕获处理。主要逻辑如下:
- 获取
promise
实例并赋值给workInProgressThrownValue
变量 - 将
SuspendedOnImmediate
赋值给workInProgressSuspendedReason
变量
handleThrow
方法执行完会进入下一循环,这时候会调用throwAndUnwindWorkLoop
方法获取SuspenseComponent
类型FiberNode
并赋值给workInProgress
,然后重新执行该FiebrNode
构建子树逻辑。
// 记录FiberNode中断渲染状态
let workInProgressSuspendedReason = NotSuspended
// 记录FiberNode终止渲染原因或promise实例
let workInProgressThrownValue = null
function handleThrow(thrownValue) {
resetHooksAfterThrow()
if (thrownValue === SuspenseException) {
// 获取promise实例
thrownValue = getSuspendedThenable()
// 记录FiberNode中断渲染状态
workInProgressSuspendedReason = SuspendedOnImmediate
}
workInProgressThrownValue = thrownValue
}
function renderRootSync(root, lanes) {
executionContext |= RenderContext
// 如果workInProgressRoot和workInProgressRootRenderLanes和本次渲染的root和lanes相同,说明是执行同一个任务
if (root !== workInProgressRoot || lanes !== workInProgressRootRenderLanes)
prepareFreshStack(root, lanes)
do {
try {
if (
workInProgressSuspendedReason !== NotSuspended &&
workInProgress !== null
) {
const unitOfWork = workInProgress
const thrownValue = workInProgressThrownValue
switch (workInProgressSuspendedReason) {
case SuspendedOnImmediate: {
const reason = workInProgressSuspendedReason
workInProgressSuspendedReason = NotSuspended
workInProgressThrownValue = null
throwAndUnwindWorkLoop(unitOfWork, thrownValue, reason)
break
}
}
}
// 递归遍历FiberNode节点,创建ReactElement对应的FiberNode节点,建立关联关系,构建FiberNode Tree
while (workInProgress !== null) {
performUnitOfWork(workInProgress)
}
break
} catch (thrownValue) {
handleThrow(thrownValue)
}
} while (true)
executionContext = NoContext
if (workInProgress === null) {
workInProgressRoot = null
workInProgressRootRenderLanes = NoLanes
}
}
三. 实现Suspense
3.1 创建Suspense
组件类型FiberNode
当从react
导出Suspense
时,其值为Symbol.for('react.suspense')
。在构建Fiber
树遇到Suspense
组件时会创建其对应的FiberNode
,其tag
属性值为SuspenseComponent
。
const REACT_SUSPENSE_TYPE = Symbol.for('react.suspense')
// Suspense组件类型FiberNode
const SuspenseComponent = 13
export { REACT_SUSPENSE_TYPE as Suspense }
3.2 构建Suspense
组件子树
3.2.1 beginWork
在构建Suspense
组件的子树节点时,如果有DidCapture
副作用,则展示fallback
组件,没有则展示异步子组件。
在2.1
小节有个throwAndUnwindWorkLoop
方法,该方法的主要作用是找到当前异步子组件的父SuspenseComponent
类型FiberNode
,将其副作用赋值为DidCapture
,另外会将promise
实例赋值给FiberNode
的updateQueue
属性。
function updateSuspenseComponent(current, workInProgress) {
const nextProps = workInProgress.pendingProps
// 是否展示fallback组件
let showFallback = false
const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags
if (didSuspend) {
showFallback = true
workInProgress.flags &= ~DidCapture
}
const nextFallbackChildren = nextProps.fallback
const nextPrimaryChildren = nextProps.children
if (current === null) {
if (showFallback) {
pushFallbackTreeSuspenseHandler(workInProgress)
const fallbackFragment = mountSuspenseFallbackChildren(
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
)
return fallbackFragment
} else {
pushPrimaryTreeSuspenseHandler(workInProgress)
return mountSuspensePrimaryChildren(workInProgress, nextPrimaryChildren)
}
}
if (showFallback) {
pushFallbackTreeSuspenseHandler(workInProgress)
return updateSuspenseFallbackChildren(
current,
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
)
} else {
pushPrimaryTreeSuspenseHandler(workInProgress)
return updateSuspensePrimaryChildren(
current,
workInProgress,
nextPrimaryChildren,
)
}
}
3.2.2 completeWork
核心逻辑主要有两个:
- 如果新旧
OffscreenComponent
类型FiberNode
的mode
不一致,添加Visibility
副作用 - 如果
FiberNode
的updateQueue
属性不为空,添加Update
副作用
function completeWork(workInProgress) {
switch (workInProgress.tag) {
case SuspenseComponent: {
popSuspenseHandler(workInProgress)
if (current !== null) {
const currentOffscreenFiber = current.child
const offscreenFiber = workInProgress.child
if (
currentOffscreenFiber.pendingProps.mode !==
offscreenFiber.pendingProps.mode
)
offscreenFiber.flags |= Visibility
}
const retryQueue = workInProgress.updateQueue
// 标记Update副作用
if (retryQueue !== null) markUpdate(workInProgress)
// 收集子树副作用
bubbleProperties(workInProgress)
return
}
}
}
3.3 更新DOM
当Fiber
树构建完成后会进入到更新DOM
阶段,核心逻辑如下:
- 对于
SuspenseComponent
类型FiberNode
,判断是否有Update
副作用,如果会获取updateQueue
属性记录的promise
实例,添加then
回调,回调逻辑是触发更新渲染 - 对于
OffscreenComponent
类型FiberNode
,根据当前mode
属性值判断是否展示或隐藏子树节点
function commitMutationEffectsOnFiber(finishedWork) {
case SuspenseComponent:
recursivelyTraverseMutationEffects(finishedWork)
if (finishedWork.flags & Update) {
const retryQueue = finishedWork.updateQueue
if (retryQueue !== null) {
finishedWork.updateQueue = null
// 添加promise实例then回调,回调逻辑是触发更细渲染
attachSuspenseRetryListeners(finishedWork, retryQueue)
}
}
break
case OffscreenComponent:
recursivelyTraverseMutationEffects(finishedWork)
if (finishedWork.flags & Visibility) {
const isHidden = finishedWork.pendingProps.mode === 'hidden'
if (isHidden) recursivelyTraverseDisappearLayoutEffects(finishedWork)
// 隐藏或显示子树节点
hideOrUnhideAllChildren(finishedWork, isHidden)
}
break
}
四. 练习题
4.1 promise
非props
传参
use
方法接收promise
并不是从props
获取,而是在组件内部创建的,读者可以判断该用法是否有问题,如果有原因是什么?
function App() {
const fetchData = new Promise(resolve => {
setTimeout(() => {
console.log(1)
resolve('hello world')
}, 1000)
})
const data = use(fetchData)
return <h1>{data}</h1>
}
4.2 不使用Suspense
组件
在下面示例中没有Suspense
组件包裹HelloWorld
组件,读者可以判断该用法是否有问题,如果有原因是什么?
function HelloWorld({ fetchData }) {
const data = use(fetchData)
return <h1>{data}</h1>
}
function App() {
const fetchData = new Promise(resolve => {
setTimeout(() => {
resolve('hello world')
}, 1000)
})
return (
<div>
<HelloWorld fetchData={fetchData} />
</div>
)
}
五. 总结
use
方法原理是判断promise
实例的状态,如果处于pending
态则添加then
回调,然后抛SuspenseException
异常,重新构建SuspenseComponent
子树。
Suspense
组件原理是判断FiberNode
是否有DidCapture
副作用,有则展示fallback
组件,否则展示异步组件。通过FiberNode
的updateQueue
记录promise
实例,在更新DOM
阶段添加then
回调,等promise
实例resolved
或rejected
时触发更新渲染。代码仓库
创作不易,如果文章对你有帮助,那点个小小的赞吧,你的支持是我持续更新的动力!