手写React Suspense组件和use方法,理解React异步操作原理

一. Suspense组件和use方法介绍

useReact提供的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时会先展示fallbackready之后再展示子组件。

二. 实现use

获取Context数据原理参考文档手写React useContext,理解useContext原理,本文不再赘述。 本文主要解析promise实例pendingresolved态的处理逻辑,rejected态的处理逻辑读者自行了解。

use方法逻辑比较简单,主要判断当前promise实例状态,如果处于pending态抛SuspenseException异常,当promise实例resolvedrejected,将对应的statusvalue记录到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实例赋值给FiberNodeupdateQueue属性。

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类型FiberNodemode不一致,添加Visibility副作用
  • 如果FiberNodeupdateQueue属性不为空,添加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 promiseprops传参

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组件,否则展示异步组件。通过FiberNodeupdateQueue记录promise实例,在更新DOM阶段添加then回调,等promise实例resolvedrejected时触发更新渲染。代码仓库

创作不易,如果文章对你有帮助,那点个小小的赞吧,你的支持是我持续更新的动力!

相关推荐
慧一居士8 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead10 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子6 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina6 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路7 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_7 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
甜瓜看代码7 小时前
1.
react.js·node.js·angular.js