React Hooks 及其性能优化之React.memo,useCallBack,useMemo

2,084

react Hooks

react hooks是react在16.8版本出现的,它的出现是为了可以只用函数组件就可以写出全功能的组件,实际上可以被认为是函数组件的加强版。
官方介绍:**Hooks是 React 16.8 中的新增功能。它们让您无需编写类即可使用状态和其他 React 功能。

本文所要总结到的memo,useCallback,useMemo即为函数组件中性能优化相关的钩子

memo

React.memo(component, myFunc)接受两个参数,一个是自定义函数,一个是比较函数。该方法类似react class组件的shouldComponentUpdate以及pureComponent, 其中第二个参数用来判断该组件需不需要重新渲染,第二个参数省略的情况下,默认会对传到该组件的props进行浅比较
以下demo在父组件中state变化了之后子组件也会触发相应的更新,但其实子组件没有接收任何props,只是渲染了一个不变的文案,那么此时这个子组件重新渲染就是没有必要的。

const ChildComponent = () => {
  console.log('子组件执行了');
  return (
    <p>我是子组件的内容</p>
  )
};

const ParentComponent = () => {
  // console.log('父组件执行了');
  const [count, setCount] = useState<Number>(1);

  const changeCount = () => {
    setCount(count + 1);
  };

  return (
    <>
      <button onClick={changeCount}>点击</button>
      <p>count is: </p>
      <p>{count}</p>
      <ChildComponent />
    </>
  )
};

image.png

在上一个demo中,我们引入React.memo。来看看控制台的打印情况

const ChildComponent = memo(() => {
  console.log('子组件执行了');
  return (
    <p>我是子组件的内容</p>
  )
});

const ParentComponent = () => {
  console.log('父组件执行了');
  const [count, setCount] = useState<Number>(1);

  const changeCount = () => {
    setCount(count + 1);
  };

  return (
    <>
      <button onClick={changeCount}>点击</button>
      <p>count is: </p>
      <p>{count}</p>
      <ChildComponent />
    </>
  )
};

image.png 此时子组件只会在组件挂载时渲染一次,父组件再怎么更新也不会触发子组件的重新渲染了。
另外,刚刚我们上面说到了memo还接收第二个参数,为自定义的比较函数,当我们传入了这个自定义函数,子组件是否重新渲染则取决于这个函数的返回值,该函数会比较新旧props是否一致,如果一致则返回true,此时子组件不会重新渲染,反之返回false,子组件会重新渲染。

const myEquareFunc = (prevProps, nextProps) => {
  console.log('prevProps is:', prevProps);
  console.log('nextProps is: ', nextProps);
  // 比较新旧props是否相等,相等返回true,否则返回false
};

useCallBack

当我们给子组件传入props后,此时用memo可能不会起作用,子组件还是会执行。 原因在于匿名函数在每次渲染后的引用都不同,从而导致子组件的重新渲染。

const ChildComponent = memo(({ text, changeText }) => {
  console.log('子组件执行了');
  return (
    <>
      <p>text is: {text}</p>
      <button onClick={()=>changeText('改变文案')}>按钮</button>
    </>
  )
});

const ParentComponent = () => {
  const [number, setNumber] = useState<number>(1);
  const [text, setText] = useState<string>('我是父组件传入子组件的文案');

  const handleChange = () => {
    setNumber(number + 1);
  };

  const changeText = (newText) => {
    setText(newText);
  };

  return (
    <>
      <button onClick={handleChange}>clike me</button>
      <p>count: {number}</p>
      <ChildComponent text={text} changeText={changeText}  />
    </>
  )
};

image.png 从以上demo可以看出,虽然子组件使用了memo,但是在父组件setNumber的每次还是会重新渲染子组件,这个时候就可以用到useCallback来优化这种行为了。

const ChildComponent = memo(({ text, changeText }) => {
  console.log('子组件执行了');
  return (
    <>
      <p>text is: {text}</p>
      <button onClick={()=>changeText('改变文案')}>按钮</button>
    </>
  )
});

const ParentComponent = () => {
  const [number, setNumber] = useState<number>(1);
  const [text, setText] = useState<string>('我是父组件传入子组件的文案');

  const handleChange = () => {
    setNumber(number + 1);
  };

  const changeText = useCallback((newText) => {
    setText(newText);
  }, []);  // 此依赖项必不可少,否则会每次渲染都会执行,从而useCallback就没有意义了

  return (
    <>
      <button onClick={handleChange}>clike me</button>
      <p>count: {number}</p>
      <ChildComponent text={text} changeText={changeText}  />
    </>
  )
};

image.png 此时就可以看到,子组件只会在初始化及点击修改文案的时候渲染一次,其他时候父组件的state再怎么更新,都不会触发子组件的重新渲染了。

useMemo

useMemo也接收两个参数,官方描述:传递一个“create”函数和一个依赖数组。useMemo仅当依赖项之一发生更改时才会重新计算记忆值。这种优化有助于避免在每次渲染时进行昂贵的计算。
useCallback(fn, deps)相当于useMemo(() => fn, deps)。 直接上demo

const ChildComponent = memo(({ infos, changeText }) => {
  console.log('子组件执行了');
  return (
    <>
      <p>text is: {infos.text}</p>
      <button onClick={()=>changeText('改变文案')}>按钮</button>
    </>
  );
});

const ParentComponent = () => {
  const [number, setNumber] = useState<number>(1);
  const [text, setText] = useState<string>('我是父组件传入子组件的文案');

  const handleChange = () => {
    setNumber(number + 1);
  };

  const changeText = useCallback((newText) => {
    setText(newText);
  }, []);  // 此依赖项必不可少,否则会每次渲染都会执行,从而useCallback就没有意义了

  return (
    <>
      <button onClick={handleChange}>clike me</button>
      <p>count: {number}</p>
      <ChildComponent infos={{text}} changeText={changeText}  />
    </>
  )
};

image.png 由以上代码可知,父组件中的state更新后,子组件还是会渲染。这是因为每次都会生成一个包含text的新对象,监听到props发生变化,自然就会重新渲染子组件。

const ChildComponent = memo(({ infos, changeText }) => {
  console.log('子组件执行了');
  return (
    <>
      <p>text is: {infos.text}</p>
      <button onClick={()=>changeText('改变文案')}>按钮</button>
    </>
  );
});

const ParentComponent = () => {
  const [number, setNumber] = useState<number>(1);
  const [text, setText] = useState<string>('我是父组件传入子组件的文案');

  const handleChange = () => {
    setNumber(number + 1);
  };

  const changeText = useCallback((newText) => {
    setText(newText);
  }, []);  // 此依赖项必不可少,否则会每次渲染都会执行,从而useCallback就没有意义了

  const result = useMemo(()=>({
    text,
  }), [text]);

  return (
    <>
      <button onClick={handleChange}>clike me</button>
      <p>count: {number}</p>
      <ChildComponent infos={result} changeText={changeText}  />
    </>
  )
};

export default ParentComponent;

image.png 当我们使用useMemo包裹所传入的对象时,则发现子组件不会再重新渲染了,这样就很好的做到了不必要的渲染。
在react官方文档中,useMemo还可以用于减少计算的量,缓存运算量比较大的函数:

const [count, setCount] = useState<number>(0);

  const expensiveFn = () => {
    console.log('方法执行了');
    let result = 0;
    for(let i = 0; i < 10000; i++) {
        result += i;
    }
    return result;
  };

  const base = expensiveFn();

  return (
    <>
      <h1>count: {count} </h1>
      <button onClick={()=>setCount(count + base)}> click me </button>
    </>
  );

image.png 这个demo每次点击按钮,组件都会重新渲染一次,方法也会重新渲染一次。但是实际上expensiceFn中是一个花费比较高的函数,且函数返回值是恒定的,这样就造成了不必要的性能浪费。 这时候就可以用useMemo将计算后的值缓存起来。

const MyComponent = () => {
  const [count, setCount] = useState<number>(0);

  const expensiveFn = () => {
    console.log('方法执行了');
    let result = 0;
    for(let i = 0; i < 10000; i++) {
        result += i;
    }
    return result;
  };

  const base = useMemo(expensiveFn, []);

  return (
    <>
      <h1>count: {count} </h1>
      <button onClick={()=>setCount(count + base)}> click me </button>
    </>
  )
};

image.png 这样的话expensiveFn的值就只会在初始的时候计算一次。 useMemo的第一个参数是一个函数,这个函数返回的值会被缓存起来,同时这个值会作为useMemo的返回值,第二个参数是一个数组依赖,如果数组里的值有变化,那么就会重新执行第一个参数里面的函数,并将函数返回的值缓存起来作为useMemo的返回值。 如果没有提供依赖数组,useMemo在每次渲染时都会计算新的值。

参考文章:
reactjs.org/docs/hooks-…
juejin.cn/post/684490…