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

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

相关推荐
亦止辰3 分钟前
AceEditor使用
前端
前端涂涂4 分钟前
nodejs中文件的重命名,移动,删除;文件夹的创建,递归创建,删除,读取;查看资源状态,批量重命名的用法,创建文件时的相对路径和绝对路径的区别和参照
前端
前端程序猿i6 分钟前
Vue组件库开发实战:从0到1构建可复用的微前端模块
前端·javascript·vue.js
幼儿园技术家13 分钟前
微信小程序/H5 调起确认收款界面
前端
键指江湖13 分钟前
React 对state进行保留和重置
javascript·react.js·ecmascript
微笑边缘的金元宝18 分钟前
Echarts柱状图斜线环纹(图形的贴花图案)
前端·javascript·echarts
wuxiguala23 分钟前
【web考试系统的设计】
前端
独立开阀者_FwtCoder2 小时前
CSS view():JavaScript 滚动动画的终结
前端·javascript·vue.js
咖啡教室2 小时前
用markdown语法制作一个好看的网址导航页面(markdown-web-nav)
前端·javascript·markdown
独立开阀者_FwtCoder2 小时前
Vue 团队“王炸”新作!又一打包工具发布!
前端·javascript·vue.js