• home > webfront > ECMAS > react >

    ReactHook详解:memo/useMemo/useCallback等钩子细讲

    Author:zhoulujun Date:

    Memo derives from memoization It means that the result of the function wrapped in React memo is saved in memory and returns the cached result if it s being called with the same input again

    什么是 memoization?

    Memoization 是优化性能的方法之一。 

    Memo derives from memoization. It means that the result of the function wrapped in React.memo is saved in memory and returns the cached result if it's being called with the same input again.

    Since it's used for pure functions: If the arguments don't change, the result doesn't change either. React.memo prevents functions from being executed in those cases.

    memoization 是一个过程,它允许我们缓存递归/昂贵的函数调用的值,以便下次使用相同的参数调用函数时,返回缓存的值而不必重新计算函数。

    注意:虽然 memoization 似乎是一个可以随处使用的巧妙小技巧,但只有在绝对需要这些性能提升时才应该使用它。 Memoization 会占用运行它的机器上的内存空间,因此可能会导致意想不到的效果。

    为什么在 React 中使用 memoization?

     React 函数组件中,当组件中的 props 发生变化时,默认情况下整个组件都会重新渲染。 换句话说,如果组件中的任何值更新,整个组件将重新渲染,包括尚未更改其 values/props 的函数/组件。

    React.memo是什么?

    React v16.6.0出了一些新的包装函数(wrapped functions),一种用于函数组件PureComponent / shouldComponentUpdate形式的React.memo()—— 它与 React.PureComponent 类似,它有助于控制 函数组件 的重新渲染。

    React.memo()是一个高阶函数,它与 React.PureComponent类似,但是

    • React.memo() 对应的是函数式组件

    • React.PureComponent 对应的是类组件。

    所谓的提升性能就是在某些场景下优化react的render渲染次数

    1. 普通组件的话+shouldComponentUpdate 

      1. true 执行 render

      2. false 不去执行 render

    2. 纯组件PureComponent 内部采用浅比较实现的(只能比较基本数据类型,遇到数组对象就虚了)

    3. 16.6版本后推出react.memo方法,可以配合函数式组件一起来去提升react性能。

    当我们需要重复使用某个组件的时候,而且传值一样,那么我们可以把组件卸载 循环里面,但这样以来,当我们某个参数发生变化的时候,会重复多次执行该组件,这样对于性能的消耗是不小的,我们只是想让受到数据影响的地方发生渲染执行,而不是全部执行

    如何使用React.memo?

    现在有一个显示时间的组件,每一秒都会重新渲染一次,对于Child组件我们肯定不希望也跟着渲染,所有需要用到PureComponent

    import React from 'react';
    
    export default class extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          date: new Date()
        }
      }
    
      componentDidMount() {
        setInterval(() => {
          this.setState({
            date: new Date()
          })
        }, 1000)
      }
    
      render() {
        return (
          <div>
            <Child seconds={1} />
            <div>{this.state.date.toString()}</div>
          </div>
        )
      }
    }


    PureComponent

    class Child extends React.PureComponent {
      render() {
        console.log('I am rendering');
        return (
          <div>I am update every {this.props.seconds} seconds</div>
        )
      }
    }

    现在新出了一个React.memo()可以满足创建纯函数而不是一个类的需求

    为何不再在函数式组件里面使用 shouldComponentUpdate?

    函数式组件里面没有声明周期钩子

    组件仅在它的 props 发生改变的时候进行重新渲染。通常来说,在组件树中 React 组件,只要有变化就会走一遍渲染流程。但是通过 PureComponent 和 React.memo(),我们可以仅仅让某些组件进行渲染

    • PureComponent 要依靠 class 才能使用。

    • 而 React.memo() 可以和 function component 一起使用


    React.memo()

    React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState,useReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。

    默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

    function Child({seconds}) {
      console.log('I am rendering');
      return (
        <div>I am update every {seconds} seconds</div>
      )
    }
    function areEqual(prevProps, nextProps) {
      if (prevProps.seconds === nextProps.seconds) {
        return true
      } else {
        return false
      }
    }
    export default React.memo(Child)

    React.memo()可接受2个参数,第一个参数为纯函数的组件,第二个参数用于对比props控制是否刷新,与shouldComponentUpdate()功能类似。

    工作原理:

    当函数计算得到结果时就将该结果按照参数存储起来。采用这种方式时,如果另外一个调用也使用相同的参数,我们则可以直接返回上次存储的结果而不是再计算一遍。像这样避免既重复又复杂的计算可以显著地提高性能

    React.memo() 与Redux

    import React from "react";
    
    function Child({seconds, state}) {
      console.log('I am rendering');
      return (
        <div>
          <div>I am update every {seconds} seconds</div>
          <p>{state}</p>
        </div>
      )
    };
    const mapStateToProps = state => ({
      state: 'React.memo()用在connect()(内)'
    });
    export default connect(mapStateToProps)(React.memo(Child))

    memoization方案在《JavaScript模式》和《JavaScript设计模式》都有提到。memoization是一种将函数执行结果用变量缓存起来的方法。当函数进行计算之前,先看缓存对象中是否有次计算结果,如果有,就直接从缓存对象中获取结果;如果没有,就进行计算,并将结果保存到缓存对象中。

    因为memo检查props的变更时采用的是标准的js相等判断逻辑

    • 对于基本数据类型执行值相等判断

    • 对于引用数据类型执行引用相等判断

    所以当组件props里的值为引用类型时,我们知道父组件的每一次render都会传递新的引用对象,memo进行相等判断时走引用相等逻辑判断,从而每次都会得到false的判断结果,也就不会阻止render过程,造成重复的渲染计算。

    既然知道了问题所在,那就好办了,只需重点关注memo组件的引用属性,保证其引用相等即可。useMemo/useCallback闪亮登场!


    什么是 useMemo()?

    React.memo() 是一个 HOC,而 useMemo() 是一个 React Hook。 使用 useMemo(),我们可以返回记忆值来避免函数的依赖项没有改变的情况下重新渲染。

    • React.memo() 是一个 HOC(高阶组件),我们可以使用它来包装我们不想重新渲染的组件,除非其中的 props 发生变化

    • useMemo() 是一个 React Hook,我们可以使用它在组件中包装函数。 我们可以使用它来确保该函数中的值仅在其依赖项之一发生变化时才重新计算

    使用 useMemo(),我们可以返回记忆值来避免函数的依赖项没有改变的情况下重新渲染。

    在某些场景下,我们只是希望 component 的部分不要进行 re-render,而不是整个 component 不要 re-render,也就是要实现 局部 Pure 功能。

    • 您可以依赖 useMemo() 作为性能优化,而不是语义保证

    • 函数内部引用的每个值也应该出现在依赖项数组中

    useMemo() 基本用法如下:

    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

    useMemo() 返回的是一个 memoized 值,只有当依赖项(比如上面的 a,b 发生变化的时候,才会重新计算这个 memoized 值)

    memoized 值不变的情况下,不会重新触发渲染逻辑。

    说起渲染逻辑,需要记住的是 useMemo() 是在 render 期间执行的,所以不能进行一些额外的副操作,比如网络请求等。

    如果没有提供依赖数组(上面的 [a,b])则每次都会重新计算 memoized 值,也就会 re-redner

    import React, { useMemo } from 'react';
    
    export default (props = {}) => {
      console.log(`--- component re-render ---`);
      return useMemo(() => {
        console.log(`--- useMemo re-render ---`);
        return <div>
          {/* <p>step is : {props.step}</p> */}
          {/* <p>count is : {props.count}</p> */}
          <p>number is : {props.number}</p>
        </div>
      }, [props.number]);
    }


    React.memo() 和 useMemo() 的主要区别

    • React.memo() 是一个高阶组件,我们可以使用它来包装我们不想重新渲染的组件,除非其中的 props 发生变化

    • useMemo() 是一个 React Hook,我们可以使用它在组件中包装函数。 我们可以使用它来确保该函数中的值仅在其依赖项之一发生变化时才重新计算

    虽然 memoization 似乎是一个可以随处使用的巧妙小技巧,但只有在绝对需要这些性能提升时才应该使用它。 Memoization 会占用运行它的机器上的内存空间,因此可能会导致意想不到的效果。


    useCallback

    React Hooks 在数据流上带来的变化有两点:

    1. 支持更友好的使用 context 进行状态管理,避免层级过多时向中间层承载无关参数;

    2. 允许函数参与到数据流中,避免向下层组件传入多余的参数。

    useContext 作为 hooks 的核心模块之一,可以获取到传入 context 的当前值,以此达到跨层通信的目的。React 官网有着详细的介绍,需要关注的是一旦 context 值发生改变,所有使用了该 context 的组件都会重新渲染。为了避免无关的组件重绘,我们需要合理的构建 context ,比如从第一节提到的新思维模式出发,按状态的相关度组织 context,将相关状态存储在同一个 context 中。

    在过去,如果父子组件用到同一个数据请求方法 getData ,而该方法又依赖于上层传入的 query 值时,通常需要将 query 和 getData 方法一起传递给子组件,子组件通过判断 query 值来决定是否重新执行 getData。

    class Parent extends React.Component {
       state = {
        query: 'keyword',
      }
    
      getData() {
        const url = `https://mocks.alibaba-inc.com/mock/fO87jdfKqX/demo/queryData.json?query=${this.state.query}`;
        // 请求数据...
        console.log(`请求路径为:${url}`);
      }
    
      render() {
        return (
          // 传递了一个子组件不渲染的 query 值
          <Child getData={this.getData} query={this.state.query} />
        );
      }
    }
    
    class Child extends React.Component {
      componentDidMount() {
        this.props.getData();
      }
    
      componentDidUpdate(prevProps) {
        // if (prevProps.getData !== this.props.getData) { // 该条件始终为 true
        //   this.props.getData();
        // }
        if (prevProps.query !== this.props.query) { // 只能借助 query 值来做判断
          this.props.getData();
        }
      }
    
      render() {
        return (
          // ...
        );
      }
    }

    在 React Hooks 中 useCallback 支持我们缓存某一函数,当且仅当依赖项发生变化时,才更新该函数。这使得我们可以在子组件中配合 useEffect ,实现按需加载。通过 hooks 的配合,使得函数不再仅仅是一个方法,而是可以作为一个值参与到应用的数据流中。

    function Parent() {
      const [count, setCount] = useState(0);
      const [query, setQuery] = useState('keyword');
    
      const getData = useCallback(() => {
        const url = `https://mocks.alibaba-inc.com/mock/fO87jdfKqX/demo/queryData.json?query=${query}`;
        // 请求数据...
        console.log(`请求路径为:${url}`);
      }, [query]);  // 当且仅当 query 改变时 getData 才更新
    
      // 计数值的变化并不会引起 Child 重新请求数据
      return (
        <>
          <h4>计数值为:{count}</h4>
          <button onClick={() => setCount(count + 1)}> +1 </button>
          <input onChange={(e) => {setQuery(e.target.value)}} />
          <Child getData={getData} />
        </>
      );
    }
    
    function Child({
      getData
    }) {
      useEffect(() => {
        getData();
      }, [getData]); // 函数可以作为依赖项参与到数据流中
    
      return (
        // ...
      );
    }


    何时useMemo和useCallback

    Performance optimizations are not free. They ALWAYS come with a cost but do NOT always come with a benefit to offset that cost.

    性能优化不是免费的。 它们总是带来成本,并且优化带来的好处并不总是能抵消成本。

    首先需要明白useMemo和useCallback内置于React的初衷:

    • 保证引用相等

    • 避免昂贵的计算

    保证引用相等

    引用相等这个问题较为基础,具体参看《JavaScript类型转换规则说明:加法 ==类型转换说明

    在react里保证引用相等可以带来什么好处呢?考虑一下useEffect的依赖列表,看一下下面的这个例子:

    组件Blub使用了Foo组件,其中Foo组件的useEffect依赖了传入的参数bar, baz。看起来很完美,只有当bar和baz改变时才会触发useEffect里的计算逻辑。

    function Foo({bar, baz}) {
      React.useEffect(() => {
        const options = {bar, baz}
        buzz(options)
      }, [bar, baz]) // we want this to re-run if bar or baz change
      return <div>foobar</div>
    }
    
    function Blub() {
      return <Foo bar="bar value" baz={3} />
    }

    那如果bar, baz其中一个或者两个都是引用类型的值呢?

    function Blub() {
      return <Foo bar={['bar','value']} baz={() => {}} />
    }

    那么Blub每次渲染时 bar, baz 都将是新的引用,所以当React测试依赖列表的值是否在渲染之间发生变化时,它将始终计算为 true,意味着每次渲染后都会调用 useEffect 回调,而不是仅在 bar 和 baz 的值更改时调用。

    问题已经明确了,怎么解决?

    这时轮到 useCallback 和 useMemo 出场了。通过引用记忆可以这样解决这类问题:

    function Foo({bar, baz}) {
      React.useEffect(() => {
        const options = {bar, baz}
        buzz(options)
      }, [bar, baz])
      return <div>foobar</div>
    }
    
    function Blub() {
      const bar = React.useMemo(() => [1, 2, 3], []);
      const baz = React.useCallback(() => {}, []);
      return <Foo bar={bar} baz={baz} />
    }

    我们使用useMemo包裹bar,使用useCallback包裹baz,实现了对变更引用的记忆,有效避免了组件更新时因为引用不一致导致的依赖列表重刷,从而触发冗余计算的问题。

    引用缓存技巧不仅对于 useEffect,对于useLayoutEffect , useCallback和 useMemo 的依赖列表元素也同样适用。


    避免昂贵的计算

    看下面的例子(这里只是举一个例子来表现“昂贵的计算”,不代表实际代码场景):

    const [count, setCount] = useState(0);
    
    const add = () => {
      setCount((prev) => prev + 1);
    };
    
    const memoAdd = useMemo(() => {
      for (let i = 0; i < 1000000; i++) {
        console.log(i);
      }
      return add;
    }, []);

    仍然是返回一个add方法,但是在返回之前增加了for (let i = 0; i < 1000000; i++),执行成本成倍增加。此时因为有useMemo的缓存加持,for循环只会在初始阶段执行一次,后续将不在执行,极大提高了性能。



    参考文章:

    Use React.memo() wisely https://dmitripavlutin.com/use-react-memo-wisely/

    How to use React.memo() to improve performance https://felixgerschau.com/react-performance-react-memo/

    react 性能提升(三) react.memo https://blog.csdn.net/qq_44163269/article/details/107324935

    如何使用React.memo() https://www.jianshu.com/p/b3d07860b778

    React.memo() 和 useMemo() 的用法和区别 https://juejin.cn/post/6991837003537088542

    你真的会用 useMemo、useCallback和memo吗? https://juejin.cn/post/7025092066614968328




    转载本站文章《ReactHook详解:memo/useMemo/useCallback等钩子细讲》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/jsBase/2021_1211_8720.html