浅谈React钩子函数 useMemo 和 useCallback

前言

React中有很多官方提供的hooks,例如 useEffect,useState,useMemo,useCallback,有部分初学者分不清 useMemo和useCallback究竟适用于什么场景。今天我们就来聊聊这两个钩子函数。

useMemo

它用于优化渲染性能useMemo 会接收一个箭头函数包裹的回调函数依赖项数组,然后返回回调函数的计算结果。当依赖项数组中的某个值发生变化时,useMemo 会重新计算回调函数。如果依赖项没有发生变化,useMemo 会返回上一次计算的结果,这样可以避免不必要的计算。如下,只有在a或者b发生改变的时候,value的值才会重新计算。

 
 const value  = useMemo(() => {
    return caculateFunction (a, b)
 }, [a, b])

使用场景:当存在一个昂贵的计算操作,且该操作的输入值在多次渲染之间不会发生变化时,应当使用useMemo 。

实现原理:在react hooks的体系中,钩子函数都有自己每个阶段的执行逻辑,并且保存在Dispatcher中,看一下挂载时的调度器如下:

 
const HooksDispatcherOnMount: Dispatcher = {
  ...
  useMemo: mountMemo,
  ...
};

更新时的调度器如下:

 
const HooksDispatcherOnUpdate: Dispatcher = {
  ...
  useMemo: updateMemo,
  ...
}

很明显,关键点在updateMemo这个方法里面。我们看一下他的实现原理

 
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;
}

这里面有一个关键函数就是areHookInputsEqual,用来对比前后两个依赖项是否发生改变

 
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  if (__DEV__) {
    if (ignorePreviousDependencies) {
      // Only true when this component is being hot reloaded.
      return false;
    }
  }

  if (prevDeps === null) {
    if (__DEV__) {
      console.error(
        '%s received a final argument during this render, but not during ' +
          'the previous render. Even though the final argument is optional, ' +
          'its type cannot change between renders.',
        currentHookNameInDev,
      );
    }
    return false;
  }

  if (__DEV__) {
    // Don't bother comparing lengths in prod because these arrays should be
    // passed inline.
    if (nextDeps.length !== prevDeps.length) {
      console.error(
        'The final argument passed to %s changed size between renders. The ' +
          'order and size of this array must remain constant.\n\n' +
          'Previous: %s\n' +
          'Incoming: %s',
        currentHookNameInDev,
        `[${prevDeps.join(', ')}]`,
        `[${nextDeps.join(', ')}]`,
      );
    }
  }
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

上述代码可以看出areHookInputsEqual 函数接受两个依赖项数组 nextDepsprevDeps。它会先检查两个数组的长度是否相等,如果不相等,将在开发模式下发出警告。然后,它遍历数组并使用 is 函数(类似于 Object.is)逐个比较元素。如果发现任何不相等的元素,函数将返回 false。否则,返回 true

这样react就知道是否需要进行重新计算操作。

useCallback

useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。

同样它也会接受两个参数,callback和依赖项, 当依赖数组中的值发生变化时,useCallback 会返回一个新的函数实例。否则,它将返回上一次创建的函数实例。useCallback 结合React.Memo进行使用。

 
 const childFucntion = useCallback(() => {
    action()
 }, [a, b])

使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。例如: 子组件如下

 
const child = memo(() => {
  return <div>
    我是子组件
  </div>
})

父组建如下:

 
const parent = props => {
  const [num, setNum] = useState(0);

  const getValue = value => {
    console.log(value);
  };

  const changeState = () => {
    setNum(pre => {
      return pre + 1;
    });
  };

  return (
    <div>
      我是父组件
      <Button onClick={changeState}>点我改变state</Button>
      {num}
      <child getValue={getValue} />
    </div>
  );
};

当点击去改变number的值时,虽然num和child没有任何关系,但是child依然会重新渲染,这很明显造成了性能浪费。更新的原因就是 React.memo 检测的是props中数据的栈地址是否改变。当你去改变父组件中的state,就会导致父组件重新构建,而父组件重新构建的时候,会重新构建父组件中的所有函数(旧函数销毁,新函数创建,等于更新了函数地址),新的函数地址传入到子组件中被props检测到栈地址更新。也就引发了子组件的重新渲染。

解决办法就是用useCallback包裹一下要传入child组件的函数。

 
const parent = props => {
  const [num, setNum] = useState(0);

  const getValue = useCallback(value => {
    console.log(value);
  }, []);

  const changeState = () => {
    setNum(val => val + 1);
  };
  
  return (
    <div>
      我是父组件
      <Button onClick={changeState}>点我改变state</Button>
      {num}
      <child getValue={getValue} />
    </div>
  );
};

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 (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

其中areHookInputsEqual跟 useMemo一样。

总结

useCallBack不要随意使用,不假思索的对每个方法增加useCallBack不要随意使用会造成不必要的性能浪费,useCallBack本身就是需要一定性能的 useCallBack并不能阻止函数重新创建,它只能通过依赖决定返回新的函数还是旧的函数,从而在依赖不变的情况下保证函数地址不变 useCallBack需要配合React.memo使用 `useMemo会执行回调函数并且返回结果,但是useCallback不会执行回调函数。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值