手写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时触发更新渲染。代码仓库

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

相关推荐
航Hang*4 分钟前
WEBSTORM前端 —— 第3章:移动 Web —— 第2节:空间转换、转化
前端·笔记·程序人生·edge·css3·html5·webstorm
霸王蟹11 分钟前
从前端工程化角度解析 Vite 打包策略:为何选择 Rollup 而非 esbuild。
前端·笔记·学习·react.js·vue·rollup·vite
EndingCoder12 分钟前
React从基础入门到高级实战:React 生态与工具 - 构建与部署
前端·javascript·react.js·前端框架·ecmascript
胡桃夹夹子15 分钟前
【前端优化】使用speed-measure-webpack-plugin分析前端运行、打包耗时,优化项目
前端·webpack·node.js
洋流23 分钟前
0基础学习,深夜写文章,励志进大厂系列,第2天:JS预编译
javascript
喝拿铁写前端30 分钟前
🏗️ 前端代码结构健康检测工具和方法:全面总结与实战指南
前端
源力祁老师33 分钟前
Odoo 中SCSS的使用指南
开发语言·前端·学习方法
北辰alk44 分钟前
package.json 中模块入口字段详解
前端
xu__yanfeng1 小时前
绘制平滑的曲线
前端
懒猫爱上鱼1 小时前
Jetpack Compose 中的 MVVM 架构解析
前端