开发者社区 > 博文 > React memo的原理、实践与思考
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

React memo的原理、实践与思考

  • 15****
  • 2024-02-23
  • IP归属:北京
  • 148浏览

    前言

    在react中,对一个组件进行点击事件等操作时,该组件以及该组件的子组件都会重新渲染。避免组件的重新渲染一般可以借助 React.memo、useCallback 等来实现。

    什么是 memo

    memo 原理

    memo 类似于 class 中 pureComponent 的特性,用于在函数式组件的父组件中对子组件进行缓存,避免在父组件重新渲染时重新渲染子组件,只有在属性发生变化时重新渲染组件。

    在 React v18.2.0 源码中,主要通过 packages/react-reconciler/src/ReactFiberBeginWork.new.js 的updateMemoComponent 方法实现 memo 的特性。

    function updateMemoComponent(
      current: Fiber | null,
      workInProgress: Fiber,
      Component: any,
      nextProps: any,
      updateLanes: Lanes,
      renderLanes: Lanes,
    ): null | Fiber {
      if (current !== null) {
        // ...
        
        const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
          current,
          renderLanes,
        );
        if (!hasScheduledUpdateOrContext) {
          // This will be the props with resolved defaultProps,
          // unlike current.memoizedProps which will be the unresolved ones.
          const prevProps = currentChild.memoizedProps;
          // Default to shallow comparison
          let compare = Component.compare;
          compare = compare !== null ? compare : shallowEqual;
          if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
            return bailoutOnAlreadyFinishedWork(
              current, 
              workInProgress, 
              renderLanes
            );
          }
        }
      }
    
      // ...
    }

    updateMemoComponent 首先会检查是否有已调度的更新或上下文更改。在存在更新时,他会去获取 memo 的 compare 方法,未自定义则取默认的比较方法 shallowEqual。去比较依赖数组中新老属性变化,确认不需要重新渲染时,会调用 bailoutOnAlreadyFinishedWork 方法来阻止组件的重新渲染。

    在 memo 的应用中,一般需要结合 useMemo、useCallback 来配合处理依赖数组和子组件的传入属性,下面将介绍这两者的原理。

    useMemo、useCallback 原理

    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 (nextDeps !== null) {
        const prevDeps: Array<mixed> | null = prevState[1];
        if (areHookInputsEqual(nextDeps, prevDeps)) {
          return prevState[0];
        }
      }
      hook.memoizedState = [callback, nextDeps];
      return callback;
    }
    
    function updateMemo<T>(
      nextCreate: () => T,
      deps: Array<mixed> | void | null,
    ): T {
      const hook = updateWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      const prevState = hook.memoizedState;
      // 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];
        }
      }
      if (shouldDoubleInvokeUserFnsInHooksDEV) {
        nextCreate();
      }
      const nextValue = nextCreate();
      hook.memoizedState = [nextValue, nextDeps];
      return nextValue;
    }

    在 React v18.2.0 源码中,主要通过 packages/react-reconciler/src/ReactFiberHooks.js 的 updateCallback 和 updateMemo 方法实现 useCallback 与 useMemo 的特性。观察源码可以发现,useMemo 与 useCallback  的实现原理基本一致,均会通过 areHookInputsEqual 方法对依赖数组的各项进行比较。不同在于返回值上,useCallback 返回回调函数,useMemo 返回数据对象。useMemo 对属性进行缓存,通过对依赖数组进行监听,更新缓存的属性。

    为什么使用 memo

    memo 原理流程图

    在这个流程中,在当前组件对应 fiber 节点的 tag 为 MemoComponent 且 没有其他更高优先级任务时,react.memo 会在新的 props 到达时,比较新旧 props 是否相等。如果相等,则不会重新渲染组件,直接返回之前的渲染结果。如果不相等,则重新执行渲染逻辑,更新组件的 DOM。这样可以避免不必要的组件重新渲染,提高性能。

    memo 使用场景

    memo 应用

    让我们从一个示例出发:

    export default const ChildCom = (props: any) => {
      const { propParam1, propParam2, func1 } = props
      console.log('child component update');
      return (
        <div onClick={func1}>
          child component
          {propParam1}
          {propParam2.a}
        </div>
      )
    }
    
    import ChildCom from './ChildCom'
    const MemoChildCom = React.memo(ChildCom)
    const App = () => {
      const [parentParam1, setParentParam1] = useState<number>(0)
      const [childParam1, setChildParam1] = useState<number>({ a: 2 })
      const childFunc1 = () => { console.log("parent param1 val: ", parentParam1) }
      return (
        <div className="App">
          <button onClick={() => setParentParam1(parentParam1+1)}>
              observe child component update
          </button>
          <MemoChildCom propParam1={1} propParam2={childParam1} func1={childFunc1}/>
        </div>
      )
    }

    代码块 memo1 中,React.memo 有两个参数,第一个为需要缓存的组件,第二个是缓存规则的方法(非必传)。对组件进行缓存后,react 会根据缓存规则的实现方法,判断是否对子组件执行重新渲染。当第二参数未传时,react 会浅比较子组件的 props 对象里各属性的变化,若全部未更新,react 在 render 中,不会给此 fiber 打上更新的 tag,不会执行重新渲染。

    注意的是,组件 ChildCom 中 props 属性 propParam2、func1 均为引用类型。在 button 被点击时,触发 setSate,App 组件会重新渲染,因此 childParam1 和 childFunc1 的引用地址会更新,在 react 进行浅比较会检测到更新, 使得 childCom 发生重新渲染。这种场景下,需要借助 useMemo 和 useCallBack 的特性。

    useMemo、useCallback应用

    useCallback 应用

    export default const ChildCom = (props: any) => {
      const { propParam1, propParam2, func1 } = props
      console.log('child component update');
      return (
        <div onClick={func1}>
          child component
          {propParam1}
          {propParam2.a}
        </div>
      )
    }
    
    import ChildCom from './ChildCom'
    const App = () => {
      const [parentParam1, setParentParam1] = useState<number>(0)
      const [childParam1, setChildParam1] = useState<number>({ a: 2 })
      const childFunc1 = () => { console.log("parent param1 val: ", parentParam1) }
    
      const callbackFunc1 = useCallback(childFunc1, [parentParam1])
      const MemoChildCom = React.memo(ChildCom, (pre, cur) => {
         if(Object.is(pre, cur) && pre.propParam2.a !== cur.propParam2.a) {
           return true
         }
         else return false
      })
      return (
        <div className="App">
          <button onClick={() => setParentParam1(parentParam1+1)}>
              observe child component update
          </button>
          <MemoChildCom propParam1={1} propParam2={childParam1} func1={callbackFunc1}/>
        </div>
      )
    }

    观察代码块 memo1 中可发现,两个引用类型的变量,其依赖于 parentParam1 和 childParam1 的属性 a。首先借助 useCallback 对方法进行缓存,并在 childCom 组件的调用上,参数 func1 传入 callbackFunc1。另外,在 React.memo 的比较策略的方法实现中,需要额外比较引用类型的变更。

    useMemo 应用

    useMemo 对于属性的缓存特性,其更多用于对复杂对象的处理并用于部分 hooks 的依赖数组上。例如 useEffect 的依赖数组中,直接监听大对象的行为是绝对要避免的,监听的对象中未参与的属性在 useState 发生变化后会引起 useEffect 不必要的执行,甚至发生死循环。这种场景一般可以借助 useMemo 进行精细化的监听操作。如下示例:

    type TAddressDTO = {
      id?: string
      provinceId: number
      provinceName: string
      cityId: number
      cityName: string
      countyId: number
      countyName: string
      townId?: number
      townName?: string
      address: string
      fullAddress?: string
      type: number
      defaultFlag?: number
      name: string
      mobile?: string
      mobileITC?: string
      encryptMobile?: string
      phone?: string
      extNumber?: string
      encryptPhone?: string
      company?: string
      shortName?: string
    }
    const memoSendAddress = useMemo(() => {
      const {
        provinceId = '',
        cityId = '',
        countyId = '',
        townId = '',
        address = '',
      } = storeSendAddress || {}
      return `${provinceId}${cityId}${countyId}${townId}${address}`
    }, [storeSendAddress])
    useEffect(() => {
      // useEffect logic
    }, [memoSendAddress])

    代码块 useMemo1 中,useEffect 针对地址对象的监听,直接监听 storeSendAddress 的话,地址大对象中任意属性的变更均未造成 useEffect logic 的执行。但实际地址对象 TAddressDTO 需要监听的属性只有各级地址。通常 useEffect logic 中会包含接口调用、数据存取等异步任务,通过 useMemo 进行筛选是很有必要的。

    需要注意的是,useMemo 第一个参数的返回值,需要为基础数据类型,否则 memoChildParam1 在 App 组件重新渲染后依然会返回新的引用地址造成 MemoChildCom 的重新渲染。

    memo 应用中的坑

    实际应用中,memo 的使用并不频繁,个人有如下的几点看法。

    1. 在代码的可维护性上,过多的使用 React.memo 对组件进行缓存并同时自定义比较方法,无形中增加了代码的复杂度,对于之后的维护上,对于他人甚至于自己,都会带来额外理解的时间;
    2. 对于 react 性能的疑虑上,diff 算法会作为最后的关卡,去优化真实 DOM 的渲染过程;
    3. 父组件重新渲染触发子组件的 render,可以避免相当一部分的 bug。笔者实际开发遇到的场景中,在例如列表渲染的诸多场景,列表的每一项往子组件传入的对象往往比较大,当对象中深层的某个属性发生改变,但由于其他 state 的变化触发父组件的重新渲染,这种触发子组件重新渲染的场景是普遍存在的。

    小结

    本文主要讲述了 React 中组件缓存中 memo的原理和一般应用,对于组件重新渲染的必要与否,更需要我们根据实际场景,测量分析缓存的消耗与节约的成本及潜在风险之间的关系,避免负优化的发生。毕竟,react 的 diff 算法会为我们最后把关。



    文章数
    1
    阅读量
    48

    作者其他文章