React.useMemo、React.useCallback 配合实现性能优化

React.useMemo、React.useCallback 配合实现性能优化


  我想,用 React 写项目的程序员都明白实现 React 性能优化的一大方式就是减少不必要的组件更新,在 Class 组件里面减少不必要的组件更新方式之一就是利用 shouldComponentUpdate 钩子函数。而在 React hooks 里面并没有此类函数,要想实现类似的功能,就需要 useMemo 和 useCallback 配合来实现。

useMemo 和 useCallback 有什么表现?

  在我的理解中, useMemo 是传入创建函数和变化依赖,根据依赖变化(浅对比)来确定是返回一个新的值还是之前的值。请看下面例子来验证我的理解:

import React, {
      ReactElement, useEffect, useMemo } from "react";

const Child1: React.FC = () => {
     
  console.log("渲染了一次本组件");
  /** 普通对象 */
  const commonObjectRef = React.useRef<ReactElement>();

  /** useMemo对象 */
  const memoObjectRef = React.useRef<ReactElement>();

  /** 组件是否主动触发过一次刷新 */
  const [isRefred, setIsRefreshed] = React.useState<boolean>(false);

  /** 声明普通对象 */
  const commonObject = <span>普通对象</span>;

  /** 声明memo 包裹的对象 */
  const memoObject = useMemo(() => <span>memo对象</span>, []);

  useEffect(() => {
     
    if (!commonObjectRef.current) {
     
      commonObjectRef.current = commonObject;
    }
    if (!memoObjectRef.current) {
     
      memoObjectRef.current = memoObject;
    }
    console.log("普通对象是否相等:", commonObjectRef.current === commonObject);
    console.log("memo对象是否相等:", memoObjectRef.current === memoObject);

    // 触发一次渲染本组件
    if (!isRefred) setIsRefreshed(true);
  }, [isRefred]);

  return <div>是否刷新过一次页面:{
     isRefred ? "是" : "否"}</div>;
};

export default Child1;

  执行结果如下图:


React.useMemo、React.useCallback 配合实现性能优化_第1张图片


  分析一下代码执行的过程:

  1. 首先是组件初次渲染,控制台打印:“渲染了一次本组件”,初次渲染期间,我们声明了保存普通对象的 Ref、保存 memo 对象的Ref、以及普通对象和 memo 对象。(在本例中,两个 Ref 你可以理解为赋值一次之后就不会变化了。)
  2. 当所有 ReactElement 都挂载好之后,执行 useEffect ,本次操作中我们用 “普通对象Ref” 保存了 “普通对象”,用 “memoRef” 保存了 “memo” 对象,打印他们是否相等,结果都为相等。(此处涉及JavaScript引用值的问题,严格来说等式两边是同一个地址),至此 React 生命周期完成一次。在最后,我们更新状态 isFreshed ,主动触发一次组件重新渲染。
  3. 组件重新渲染,控制台打印 “渲染了一次本组件”, 且我们又重新声明了 “普通对象” ,“memo对象”,然后挂载元素。
  4. 因为我们依赖了 isFreshed ,它由 false 变为了 true,因此由执行 useEffect。之前说了本例 Ref 赋值之后就不会变了,我们判断 Ref 保存的值是否和本次生成的值相等。可以看到,普通对象是不等的,而 memo 对象是相等的。

   由此可见,useMemo 在没有传入依赖项的时候,返回的值是不会变的,即使返回的是引用类型,它返回的内存地址也是不会变的,它只会在依赖数组中的值有变化的时候才返回新的值。

   useCalback 和 useMemo 行为类似,它们的不同在于 useCallback 返回的是你传入的函数,useMemo 返回的是你传入的函数执行后返回的值。


实现性能优化

  在 React 函数里面,如果父组件更新,要让子组件不更新,需要满足两个条件:

  1. 子组件不变,在 React 虚拟 DOM 里面,组件是一个对象,也就是说,我们需要保持这个组件(对象)的引用地址不变,这样父组件在浅对比的时判断组件是没有变化的。
  2. 传入子组件的 Props 在浅对比下是不变的。

  针对第一条,利用刚才在 useMemo 的表现描述中能够想到: 保持地址引用不变,我们只需要用 useMemo 返回这个子组件,依赖数组传入它的更新依赖就可以了。(注: 如果直接在父组件 return 形式的组件(对象) ,很显然此时返回的是一个新对象。)


  针对第二条,简单数据类型来说,只要他们值是相等的,就可以认为是没有变化。复杂数据类型对象(数组、对象、函数)来说,在父组件函数内部声明的复杂数据对象都是新的,此时浅对比下 React 认为组件发生了变化需要更新。我们要做的是返回旧的对象从而满足第二个条件。

  所以为了满足两个条件,我们可以使用 useMemo 包裹返回不变的对象(组件、数组、对象、函数),使用 useCallback 返回一个不变的函数。(注: 我没有验证过,但我觉得 useMemo 返回一个函数功能应该和 useCallback 效果是一样的,只是语义化效果不好,哈哈)。

Demo
import React from "react";
import ReactDOM from "react-dom";

import Child1 from "./component/child1/child1";
import Child2 from "./component/child2/child2";

const Father = () => {
     
  console.log("更新父组件");

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

  React.useEffect(() => {
     
    const Fresh = () => {
     
      setTimeout(() => {
     
        setCount((count) => count + 1);
        Fresh();
      }, 1000);
    };
    Fresh();
  }, []);

  return (
    <div>
      <p>父组件更新计数:{
     count}</p>

      <Child1 />

      {
     React.useMemo(
        () => (
          <Child2 />
        ),
        []
      )}
    </div>
  );
};

ReactDOM.render(<Father />, document.getElementById("root"));

import React from "react";

const Child1: React.FC = () => {
     
  console.log("更新子组件1");
  return <div>子组件1</div>;
};

export default Child1;

import React from "react";

const Child2: React.FC = () => {
     
  console.log("更新子组件2");
  return <div>子组件2</div>;
};
export default Child2;



  执行结果:

React.useMemo、React.useCallback 配合实现性能优化_第2张图片
  可以看到 初始化一次之后再没有更新,如果在实际应用中 传入了父组件中的会变的对象,记得用 useMemo 或 useCallback(处理函数) 处理,阻止组件不必要的更新以达到性能优化的目的。

你可能感兴趣的:(React,hooks,react)