一. Suspense
组件和use
方法介绍
use
是React
提供的API
,有两个作用,一个是获取Context
数据,等价于useContext
,另一个作用是接收promise
,常见场景是请求数据。需要注意的是如果接收promise
,需要配合Suspense
组件。
代码示例如下,执行流程首先是展示Loading
态,一秒之后展示hello world
javascript
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
实例上。
javascript
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
构建子树逻辑。
javascript
// 记录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
。
javascript
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
属性。
javascript
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
副作用
javascript
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
属性值判断是否展示或隐藏子树节点
javascript
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
获取,而是在组件内部创建的,读者可以判断该用法是否有问题,如果有原因是什么?
javascript
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
组件,读者可以判断该用法是否有问题,如果有原因是什么?
javascript
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
时触发更新渲染。代码仓库
创作不易,如果文章对你有帮助,那点个小小的赞吧,你的支持是我持续更新的动力!