手写React useCallback,理解useCallback原理

64 阅读3分钟

一. useCallback方法介绍

useCallback方法接收两个参数,第一个参数是cache function,第二个参数是依赖deps,首次渲染调用会直接返回cache function,更新渲染会比对deps是否相同,不相同返回新的cache function

function App() {
  const handleClick = useCallback(() => {}, [])
  
  return <h1 onClick={handleClick}>click</h1>
}

二. 实现useCallback

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 首次调用useCallback

当首次执行组件方法调用useCallback方法时,执行mountCallback方法逻辑

  • 创建Hook对象,构建Hook链表
  • callbackdeps保存到Hook对象的memoizedState属性
  • 返回callback
function mountWorkInProgressHook() {
  // 创建Hook对象
  const hook = new Hook()
  // 构建Hook链表
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook
  } else {
    workInProgressHook = workInProgressHook.next = hook
  }
  return hook
}

function mountCallback(callback, deps) {
  // 创建Hook对象,构建Hook单链表
  const hook = mountWorkInProgressHook()
  // 将callback和deps保存到Hook对象的memoizedState属性
  hook.memoizedState = [callback, deps]
  return callback
}

2.4 更新调用useCallback

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

  • 创建Hook对象,复制旧Hook对象属性值,构建Hook链表
  • 获取旧函数和依赖deps,比对新旧deps是否相同,相同则返回旧函数,不相同则返回新函数
  • 将新函数和deps保存到Hook对象的memoizedState属性
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
}

// 通过Object.is方法比对deps属性值是否变更
function areHookInputsEqual(nextDeps, prevDeps) {
  for (let i = 0; i < nextDeps.length; i++) {
    if (!Object.is(nextDeps[i], prevDeps[i])) {
      return false
    }
  }
  return true
}

function updateCallback(callback, deps) {
  // 创建Hook对象,复制旧Hook对象属性值,构建Hook链表
  const hook = updateWorkInProgressHook()
  // 获取旧函数和依赖deps
  const prevState = hook.memoizedState
  // 比对新旧deps是否相同,相同则直接返回旧值
  if (deps !== null && areHookInputsEqual(deps, prevState[1])) {
    return prevState[0]
  }
  // 将新函数和deps保存到Hook对象的memoizedState属性
  hook.memoizedState = [callback, deps]
  return callback
}

2.5 定义useCallback方法

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

function useCallback(callback, deps = null) {
  const current = currentlyRenderingFiber.alternate
  if (current === null) {
    return mountCallback(callback, deps)
  } else {
    return updateCallback(callback, deps)
  }
}

三. 往期文章推荐

3.1 React原理系列总结

四. 参考文档

4.1 React useCallback官方文档