likes
comments
collection
share

React Hook实现原理之useCallback与useMemo

作者站长头像
站长
· 阅读数 11

本章节将讲解两个关于性能优化的hookuseCallbackuseMemo

本节主要是介绍这两个hook的实现原理,关于函数组件更新优化的具体逻辑会在后面新的章节介绍。

1,useCallback

这个hook的作用是返回一个固定引用地址的函数,相当于缓存一个声明的函数,通常用它进行性能优化。

const cachedFn = useCallback(fn, dependencies)
import { useState, useCallback } from 'react'

export default function MyFun(props) {
  console.log('MyFun组件运行了')
  const [count, setCount] = useState(1)
  const testFn = useCallback(() => {
    console.log('测试useCallback')
  }, [])
  function handleClick() {
    setCount(2)
  }
  return (
    <div className='MyFun'>
      <div>state: {count}</div>
      <button onClick={handleClick}>更新</button>
    </div>
  )
}

下面我们开始解析它的实现原理。

加载阶段处理

在函数组件加载时,会调用一次函数MyFun,这时就会开始对我们定义的hook进行加载处理:

const HooksDispatcherOnMount: Dispatcher = {
    useCallback: mountCallback,
}

查看mountCallback方法:

// packages\react-reconciler\src\ReactFiberHooks.new.js

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

mountWorkInProgressHook方法是每个hook加载时都会调用的一个公用方法,它的作用是创建一个内部hook对象,这个对象会存储我们定义的hook相关信息,如果一个函数组件中使用了多个hook,则mountWorkInProgressHook方法内部会将它们按加载顺序构建成一个单向hook链表,存储到函数组件Fiber节点的memoizedState属性之上,如图所示:

React Hook实现原理之useCallback与useMemo

这个对象有以下几个属性:

const hook: Hook = {
  memoizedState: null, // 存储hook数据或者回调函数
  baseState: null, 
  baseQueue: null,
  queue: null,
  next: null, // 指向下一个hook对象
};

这里我们只需要关注memoizedState属性,因为useCallback只使用了这个属性:

hook.memoizedState = [callback, nextDeps];

hook对象的memoizedState属性设置为一个数组,它只有两位元素,存储的就是我们传入的callback回调函数和deps依赖。

return callback;

最后直接返回callback回调函数,useCallback hook的加载就处理完成,也是比较简单的。

更新阶段处理

在函数组件更新时,同样会调用一次函数MyFun,这时就会开始对我们定义的hook进行更新处理:

const HooksDispatcherOnUpdate: Dispatcher = {
    useCallback: updateCallback,
}

查看updateCallback方法:

// packages\react-reconciler\src\ReactFiberHooks.new.js

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  // 新的依赖数据
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      // 旧的依赖数据
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

updateWorkInProgressHook方法是每个hook更新时都会调用的一个公用方法,它的作用是根据函数组件加载时形成的hook链表,找到该hook对应的原内部hook对象,复用该对象信息,创建新内部hook对象。

注意: 这里的重点就是复用了原内部hook对象的信息,因为前面我们已经知道,在加载阶段我们的传入callback回调函数存储在hook.memoizedState属性中,在我们复用了原对象信息后,就可以拿到原来的回调函数。

const prevState = hook.memoizedState; //  [callback, nextDeps]

此时的prevState就是我们需要的内容,在我们拿到原数据之后,有一个依赖变化的判断,这个就是useCallback缓存的重点。

// 新的依赖数据
const nextDeps = deps === undefined ? null : deps;
// 旧的依赖数据
const prevDeps = prevState[1];

在确定新旧依赖数据之后,调用了一个areHookInputsEqual方法来判断依赖是否发生变化。

如果依赖没有发生变化,则返回true,表明新旧依赖相等,则直接返回原callback回调函数。

return prevState[0];

这就是useCallback hook的缓存功能的实现,它的本质就是将callback回调函数存储到内部hook对象的memoizedState属性之上,然后这个hook对象会存储到函数组件的Fiber节点上,只要函数组件一直存在,则此callback会被一直缓存。

以上就是useCallback的实现原理,也是比较简单的。

areHookInputsEqual

最后我们再来看一下比较依赖变化的方法areHookInputsEqual,其实它的逻辑也非常简单,我们一看便知。

查看areHookInputsEqual方法:

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {

  // 情况1,无依赖参数,每次渲染都会执行副作用
  if (prevDeps === null) {
    return false;
  }
  
  // 情况2,有至少一项依赖参数,循环判断每个依赖是否相等,任何一个依赖变化则会重新执行副作用
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  // 情况3,参数为空数组或者所有依赖都没有变化的情况,重新渲染不执行副作用
  return true;
}

根据此方法校验逻辑,可以分为以下三种情况:

情况一: 如果prevDepsnull,代表没有依赖参数,此时直接返回false,则函数组件每次渲染之后,都会执行此副作用回调。

情况二: 参数存在且有至少一个依赖项,则循环每个依赖,使用Object.is判断新旧依赖是否变化,任何一个依赖变化都会返回false,则本次更新后会执行副作用回调。如果都没有变化,最终会返回true,则不会执行副作用回调。

情况三: 参数为空数组或者所有依赖都没有变化的情况,直接返回true,组件更新不会执行副作用回调。

2,useMemo

这个hook的作用是在函数组件重新渲染的时候能够缓存计算的结果,通常用它进行性能优化。

const cachedValue = useMemo(calculateValue, dependencies)

useMemo用于保持一些比较稳定的数据,它的功能类似于Vue中的计算属性computed,依赖项没有变化,返回值就不会变化。

这个hook比useCallback更加强大,useCallback只是固定函数的引用,useMemo可以固定所有返回值的引用或者说结果。

import { useState, useMemo } from 'react'

export default function MyFun(props) {
  console.log('MyFun组件运行了')
  const [count, setCount] = useState(1)
  // 缓存计算的结果
  const countPx = useMemo(() => count + 'px', [count])
  function handleClick() {
    setCount(2)
  }
  return (
    <div className='MyFun'>
      <div>state: {count}</div>
      <button onClick={handleClick}>更新</button>
    </div>
  )
}
  • useMemo返回一个计算结果时,它的效果与Vuecomputed基本一致,它们的区别在于computed自动收集依赖,useMemo需要我们手动添加依赖。
const countPx = useMemo(() => count + 'px', [count])
const countPx = computed(() => count + 'px')
  • useMemo返回一个函数时,它的效果与useCallback基本一致。
const fn = useMemo(() => {
    return () => {
        console.log('useMemo')
    }
}, [])
const fn = useCallback(() => {
    console.log('useCallback')
}, [])

computed在需要传递参数时也可以返回一个函数,此时和useMemo功能完全一样。

下面我们开始解析它的实现原理。

加载阶段处理

在函数组件加载时,会调用一次函数MyFun,这时就会开始对我们定义的hook进行加载处理:

const HooksDispatcherOnMount: Dispatcher = {
    useMemo: mountMemo,
}

查看mountMemo方法:

// packages\react-reconciler\src\ReactFiberHooks.new.js

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 执行回调函数,生成初始结果
  const nextValue = nextCreate();
  // 缓存初始结果
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

根据源码就可以看useMemouseCallback的实现原理基本一致,唯一的区别就是:

  • useCallback是直接缓存的我们传入的回调函数。
  • useMemo是执行一次我们传入的回调函数,缓存的是执行后的结果。
更新阶段处理

在函数组件更新时,同样会调用一次函数MyFun,这时就会开始对我们定义的hook进行更新处理:

const HooksDispatcherOnUpdate: Dispatcher = {
    useCallback: updateMemo,
}

查看updateMemo方法:

// packages\react-reconciler\src\ReactFiberHooks.new.js

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

根据updateMemo源码可以看出,更新阶段useMemouseCallback也是一样的实现原理。useMemo根据依赖是否变化来决定是否重新调用一次回调函数,缓存新的计算结果。区别就只是useMemo缓存回调函数计算后的结果,useCallback直接缓存回调函数。

最后我们再来做一个总结: 根据源码我们可以发现useCallbackuseMemo实现原理是基本一样的。这种实现react框架中比较常见的,比如useStateuseReduceruseEffectuseLayoutEffect等等,它们都是通过一个小的标识或者细节来区分两个API,而框架选择新增一个API而不是采用配置项,目的都是为了方便开发者的使用。

结束语

以上就是useCallbackuseMemo实现原理的全部内容了,觉得有用的可以点赞收藏!如果有问题或建议,欢迎留言讨论!