美文网首页
React Hook - 如何使用useMemo和useCall

React Hook - 如何使用useMemo和useCall

作者: sunnyaxin | 来源:发表于2022-10-22 20:34 被阅读0次

    Hook是在React 16.8之后增加的一项新功能,能够帮助我们在不写class的情况下使用state和其他React的相关特性。关于如何使用Hook官网有很多介绍,但理论是一码事,实践又是另一码事。

    如果写过一段时间的React,我们知道通过使用 useMemouseCallback 能够帮助我们的代码提高性能,避免component在re-render时的无效计算。于是,慢慢的大家会发现前端代码里到处都是 useMemouseCallback,不仅影响代码的可读性,而且也不利于debug。

    本文会通过举例等方式告诉大家,其实我们前端工程中几乎90%的 useMemouseCallback 都是可以移除的,而且代码不会有任何问题,且启动加载速度会更快。

    为什么需要 useMemo和 useCallback

    为了在re-render之间能够进行缓存。如果hook包裹了某个值或者方法,react会在初始渲染时对其进行缓存,并在多次渲染时对该保存值进行引用。如果没有hook,非原生类型如数组、对象和方法会在每次重新渲染时重新创建。例如:

    const a = { "test": 1 };
    const b = { "test": 1'};
    
    console.log(a === b); // will be false
    
    const c = a; // "c" is just a reference to "a"
    
    console.log(a === c); // will be true
    

    另一个更贴近react代码的例子:

    const Component = () => {
      const a = { test: 1 };
    
      useEffect(() => {
        // "a" will be compared between re-renders
      }, [a]);
    
      // the rest of the code
    };
    

    auseEffect的一个依赖值,React每次重新渲染 Component 时都会与其之前的值进行对比。a是在 Component 中定义的一个对象,因此每次重新渲染都会重新创建。因此“渲染前”的a与“渲染后”的 a比较结果为不想等,因此 useEffect 也会在每次重新渲染时被触发。
    为了避免上述情况,我们可以对a使用 useMemo

    const Component = () => {
      // preserving "a" reference between re-renders
      const a = useMemo(() => ({ test: 1 }), []);
    
      useEffect(() => {
        // this will be triggered only when "a" value actually changes
      }, [a]);
    
      // the rest of the code
    };
    

    现在只有当a的值真的发生变化时才会触发useEffect
    useCallback的使用与上述类似,只是它更常用于方法:

    const Component = () => {
      // preserving onClick function between re-renders
      const fetch = useCallback(() => {
        console.log('fetch some data here');
      }, []);
    
      useEffect(() => {
        // this will be triggered only when "fetch" value actually changes
        fetch();
      }, [fetch]);
    
      // the rest of the code
    };
    

    这里要注意,useMemouseCallback只有在重新渲染期间才有用,在初始渲染时,他们的使用会导致react做更多的事情,所以程序也会更慢一些。如果你的代码到处都是用了成百上千个hook,这种减速甚至是肉眼可感知的。

    为什么Component会重新渲染

    两种场景:

    1. 当Component的state或者prop发生变化时,React会重新渲染该Component
    2. 当父Component被重新渲染时
      即当一个Component重新渲染它自己时,它也会重新渲染它所有的子Component,例如:
    const App = () => {
      const [state, setState] = useState(1);
    
      return (
        <div className="App">
          <button onClick={() => setState(state + 1)}> click to re-render {state}</button>
          <br />
          <Page />
        </div>
      );
    };
    

    App Component有一些state也有一些自Component,例如 Page,当点击页面上的button,state会发生改变,因此会触发 App的重新渲染,因此会触发其所有子Component的重新渲染,包括 Page,即使他没有props。
    而如果在 Page 内部,还有一些其他子Component:

    const Page = () => <Item />;
    

    即使该子Component没有state也没有props,但也会由于App的重新渲染而被触发重新渲染。由此得出结论,App由于其state变化而触发的重新渲染,会触发整个程序的重新渲染链。
    如何中断这条重新渲染链呢?对子Component进行缓存:

    1. 使用 useMemo hook
    2. 使用 React.memo 工具
      只有通过上面两种方法,React才会在重新渲染之前停止并检查其props值是否改变:
    const Page = () => <Item />;
    const PageMemoized = React.memo(Page);
    
    const App = () => {
      const [state, setState] = useState(1);
    
      return (
        ... // same code as before
          <PageMemoized />
      );
    };
    

    有且只有在上述情况下,讨论props是否被缓存才有意义。

    例如:

    const App = () => {
      const [state, setState] = useState(1);
      const onClick = () => {
        console.log('Do something on click');
      };
      return (
        // page will re-render regardless of whether onClick is memoized or not
        <Page onClick={onClick} />
      );
    };
    

    如果 Page没有被缓存,当 App 重新渲染时,React发现 Page是其子Component,就会也重新渲染它。那么不论 onClick 是否使用useCallback都是没有意义的;

    对上述例子进一步优化,缓存 Page:

    const PageMemoized = React.memo(Page);
    
    const App = () => {
      const [state, setState] = useState(1);
      const onClick = () => {
        console.log('Do something on click');
      };
      return (
        // PageMemoized WILL re-render because onClick is not memoized
        <PageMemoized onClick={onClick} />
      );
    };
    

    如果 Page被缓存,当 App 重新渲染时,React发现 PageMemoized是其子Component且已使用 React.memo,因此中断重新渲染链条,首先检查 PageMemoized 的 props 是否发生变化。上述例子中,由于 onClick 没有被缓存,所以props发生变化,PageMemoized 会被重新渲染;

    对上述例子继续优化,缓存 Page,使用 useCallback :

    const PageMemoized = React.memo(Page);
    
    const App = () => {
      const [state, setState] = useState(1);
      const onClick = useCallback(() => {
        console.log('Do something on click');
      }, []);
    
      return (
        // PageMemoized will NOT re-render because onClick is memoized
        <PageMemoized onClick={onClick} />
      );
    };
    

    那么,React在PageMemoized 上停止重新渲染链并检查其props,发现 onClick没有发生变化,因此PageMemoized 也不会被重新渲染;

    对上述例子继续变种,在PageMemoized 上增加另一个没有缓存的值:

    const PageMemoized = React.memo(Page);
    
    const App = () => {
      const [state, setState] = useState(1);
      const onClick = useCallback(() => {
        console.log('Do something on click');
      }, []);
    
      return (
        // page WILL re-render because value is not memoized
        <PageMemoized onClick={onClick} value={[1, 2, 3]} />
      );
    };
    

    那么,React在PageMemoized 上停止重新渲染链并检查其props,发现 onClick没有发生变化,但是value发生变化,因此PageMemoized 会被重新渲染;

    综上所述,得出结论:只有Component本身和它的每一个props都被缓存时,hook的优化才有意义。否则都是对内存的浪费,且会降低代码的可读性。

    参考文章

    How to useMemo and useCallback: you can remove most of them

    相关文章

      网友评论

          本文标题:React Hook - 如何使用useMemo和useCall

          本文链接:https://www.haomeiwen.com/subject/yznizrtx.html