手写React useSyncExternalStore,理解useSyncExternalStore原理

58 阅读3分钟

一. useSyncExternalStore方法介绍

useSyncExternalStore方法接收三个参数,第一个参数是监听事件函数,第二个参数是获取数据方法,第三个参数是服务端渲染时调用的方法

例如下面这段,监听windowonlineoffline事件,当事件触发时会调用getSnapshot方法获取新的返回值

function getSnapshot() {
  return navigator.onLine
}

function subscribe(callback) {
  window.addEventListener('online', callback)
  window.addEventListener('offline', callback)

  return () => {
    window.removeEventListener('online', callback)
    window.removeEventListener('offline', callback)
  }
}

function App() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot)

  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>
}

二. 实现useSyncExternalStore

useSyncExternalStore内部实现依赖useEffect,可以参考文章手写React useEffect,理解useEffect原理

2.1 定义Hook对象原型

每次调用React Hook方法都会创建一个Hook对象,多个Hook对象之间通过next指针进行索引,构成单链表数据结构

function Hook() {
  this.memoizedState = null // 记录Hook数据
  this.next = null // 记录下一个Hook对象
}

2.2 定义函数组件方法调用装饰器

在构建虚拟DOM树阶段,每次调用函数组件方法(例如App Component Function)时会执行renderWithHooks方法,记录新FiberNode节点,在调用React Hook方法时会用到。

// 记录新FiberNode节点
let currentlyRenderingFiber = null
// 记录旧Hook对象
let currentHook = null
// 记录新Hook对象
let workInProgressHook = null

/** 
 * @param {*} workInProgress 新FiberNode节点
 * @param {*} Component 函数组件方法
 * @param {*} props 函数组件方法入参属性
*/
export function renderWithHooks(workInProgress, Component, props) {
  // 记录新FiberNode节点
  currentlyRenderingFiber = workInProgress
  // 将FiberNode节点的updateQueue属性赋值为null,重新收集useEffect、useLayoutEffect、useInsertionEffect数据
  workInProgress.updateQueue = null
  // 调用组件方法获取child ReactElement
  const children = Component(props)
  currentlyRenderingFiber = null
  currentHook = null
  workInProgressHook = null
  return children
}

2.3 首次调用useSyncExternalStore

当首次执行组件方法调用useSyncExternalStore方法时,执行mountSyncExternalStore方法逻辑

  • 创建Hook对象,构建Hook链表
  • 监听subscribe方法,在DOM更新阶段调用subscribe方法
  • 调用getSnapshot方法获取初始值,将初始值和getSnapshot方法赋值给Hook对象的memoizedState属性,返回初始值
function forceStoreRerender(fiber) {
  // 获取FiberRootNode对象
  const root = getRootForUpdatedFiber(fiber)
  // 修改FiberNode节点优先级
  fiber.lanes |= SyncLane
  if (fiber.alternate !== null) fiber.alternate.lanes |= SyncLane
  // 触发更新渲染
  scheduleUpdateOnFiber(root, SyncLane)
}

// 调用subscribe方法,并获取返回的destroy方法
function subscribeToStore(fiber, hook, subscribe) {
  // 当触发监听事件时,会调用callback方法
  const callback = () => {
    const [prevSnapshot, getSnapshot] = hook.memoizedState
    const nextSnapshot = getSnapshot()
    // 比对新旧值是否相同,如果不相同更新Hook对象memoizedState属性,触发更新渲染
    if (nextSnapshot !== prevSnapshot) {
      hook.memoizedState[0] = nextSnapshot
      forceStoreRerender(fiber)
    }
  }
  return subscribe(callback)
}

function mountWorkInProgressHook() {
  // 创建Hook对象
  const hook = new Hook()
  // 构建Hook链表
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook
  } else {
    workInProgressHook = workInProgressHook.next = hook
  }
  return hook
}

// 为了便于理解,这里不讨论传入服务端渲染调用方法的情况
function mountSyncExternalStore(subscribe, getSnapshot) {
  // 创建Hook对象,构建Hook单链表
  const hook = mountWorkInProgressHook()
  // 监听subscribe函数,在DOM更新阶段调用subscribe方法
  mountEffect(
    subscribeToStore.bind(null, currentlyRenderingFiber, hook, subscribe),
    [subscribe],
  )
  // 调用getSnapshot方法获取初始值
  const nextSnapshot = getSnapshot()
  hook.memoizedState = [nextSnapshot, getSnapshot]
  return nextSnapshot
}

2.4 更新调用useSyncExternalStore

当触发更新渲染重新执行组件方法调用useSyncExternalStore方法时,执行updateSyncExternalStore方法逻辑

  • 创建Hook对象,复制旧Hook对象属性值,构建Hook链表
  • 重新赋值getSnapshot方法,确保调用最新的getSnapshot方法
  • 监听subscribe方法,如果发生变更,会在DOM更新阶段重新执行subscribe方法
function updateWorkInProgressHook() {
  // 获取旧Hook对象
  if (currentHook === null) {
    currentHook = currentlyRenderingFiber.alternate.memoizedState
  } else {
    currentHook = currentHook.next
  }
  // 创建新Hook对象,复制旧Hook对象属性值
  const hook = new Hook()
  hook.memoizedState = currentHook.memoizedState
  // 构建Hook链表
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook
  } else {
    workInProgressHook = workInProgressHook.next = hook
  }
  return hook
}

// 为了便于理解,这里不讨论传入服务端渲染调用方法的情况
function updateSyncExternalStore(subscribe, getSnapshot) {
  // 创建Hook对象,复制旧Hook对象属性值,构建Hook链表
  const hook = updateWorkInProgressHook()
  // 由于getSnapshot方法可能发生变更,所以需要重新赋值确保调用最新的getSnapshot方法
  hook.memoizedState[1] = getSnapshot
  // 监听subscribe方法,如果发生变更,会在DOM更新阶段重新执行subscribe方法
  updateEffect(
    subscribeToStore.bind(null, currentlyRenderingFiber, hook, subscribe),
    [subscribe],
  )
  return hook.memoizedState[0]
}

2.5 定义useSyncExternalStore方法

如果新节点不存在旧FiberNode节点,说明是首次调用函数组件方法,则调用mountSyncExternalStore方法,否则调用updateSyncExternalStore方法

export function useSyncExternalStore(subscribe, getSnapshot) {
  const current = currentlyRenderingFiber.alternate
  if (current === null) {
    return mountSyncExternalStore(subscribe, getSnapshot)
  } else {
    return updateSyncExternalStore(subscribe, getSnapshot)
  }
}

三. 往期文章推荐

3.1 React原理系列总结

四. 参考文档

4.1 React useSyncExternalStore官方文档