React 函数组件 - 性能优化篇

368 阅读8分钟

这是我参与2022首次更文挑战的第41天,活动详情查看:2022首次更文挑战

React 函数组件 - 性能优化篇

前言

前段时间,在使用 React做H5项目的时候,使用Class组件Hooks组件开发项目的过程中,发现函数组件的props不管有没有发生变化,只要父组件状态更改了,那么所有的子组件都会被更新,我们一起来看看吧

函数组件特性

  • React中的hooks组件本质就是函数组件, 函数组件是由函数实现的,公式:F(Props) => UI;即数据映射视图
  • 对于函数组件来说,每次状态更新,都会导致组件更新
  • 每次组件更新,组件内部是事件处理程序都会重新执行,子组件接受了新的props也会进行更新,这样就会产生一定的性能问题

举个栗子

import { useState } from "react";
import ReactDom from "react-dom";

function SubChildren() {
  console.log("子组件更新了");
  return <div>子组件</div>;
}

function App() {
  console.log("App父组件");

  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <SubChildren />
    </div>
  );
}

ReactDom.render(<App />, document.getElementById("root"));

image.png

  • 这是一个计数器+1的栗子
  • 页面刚加载的时候,子组件会被执行,没有问题
  • 当我们执行+1操作的时候,SubChildren 组件依旧会被更新
  • 此时的SubChildren组件不依赖外部的props,当父组件执行+1操作的时候,我们不希望子组件重新执行

React.memo

介绍

  • React.memo 本质是一个高阶组件。

  • 如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

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

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

代码改造

import { memo, useState } from "react";
import ReactDom from "react-dom";

type SubProps = {
 name: string;
};
function SubChildren({ name }: SubProps) {
 console.log("子组件更新了");
 return <div>子组件 {name}</div>;
}

const SubChildrenMemo = memo(SubChildren);

function App() {
 console.log("App父组件");
 const [count, setCount] = useState(0);
 return (
   <div>
     <p>{count}</p>
     <button onClick={() => setCount(count + 1)}>Click me</button>
     <SubChildrenMemo name="张三" />
   </div>
 );
}

ReactDom.render(<App />, document.getElementById("root"));

image.png

  • 这个时候子组件只有在第一次加载的时候会被调用

React.memo遇到了回调函数

import { memo, useState } from "react";
import ReactDom from "react-dom";

type SubProps = {
 name: string;
 onClick: () => void;
};
function SubChildren({ name, onClick }: SubProps) {
 console.log("子组件更新了");
 return <div onClick={onClick}>子组件 {name}</div>;
}

const SubChildrenMemo = memo(SubChildren);

function App() {
 console.log("App父组件");
 const [count, setCount] = useState(0);
 const handleClickSub = () => {
   console.log("子组件点击事件");
 };
 return (
   <div>
     <p>{count}</p>
     <button onClick={() => setCount(count + 1)}>Click me</button>
     <SubChildrenMemo name="张三" onClick={handleClickSub} />
   </div>
 );
}

ReactDom.render(<App />, document.getElementById("root"));

image.png

  • 当子组件添加了回调函数的时候,每次父组件执行+1操作的时候,子组件依旧会被更新
  • 这个时候心里一万头那啥跑过
  • 仔细阅读文档后发现React.memo 默认情况下其只会对复杂对象做浅层对比

React.memo + UseCallback解决事件回调问题

  • 介绍
    • 写法

          const memoizedCallback = useCallback(
          () => {
              doSomething(a, b);
          },
          [a, b],
          );
      
    • 返回一个 memoized 回调函数。

    • 把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

    • useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

  • 写法
        import { memo, useCallback, useState } from "react";
        import ReactDom from "react-dom";
    
        type SubProps = {
        name: string;
        onClick: () => void;
        };
        function SubChildren({ name, onClick }: SubProps) {
        console.log("子组件更新了");
        return <div onClick={onClick}>子组件 {name}</div>;
        }
    
        const SubChildrenMemo = memo(SubChildren);
    
        function App() {
        console.log("App父组件");
        const [count, setCount] = useState(0);
        const handleClickSub = useCallback(() => {
            console.log("子组件点击事件");
        }, []);
        return (
            <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>Click me</button>
            <SubChildrenMemo name="张三" onClick={handleClickSub} />
            </div>
        );
        }
    
        ReactDom.render(<App />, document.getElementById("root"));
    
    

image.png 说明 - UseCallBack接收俩个参数,第一个是事件处理函数,第二个是依赖项,没有依赖项就传入[],UseCallBack的第二个参数和useEffect的第二个参数具有相同的作用

如果在子组件的回调函数中处理父组件的属性,那么只需要添加对应的依赖项即可

  • 举个栗子
        const handleClickSub = useCallback(() => {
            console.log("子组件点击事件", count);
        }, [count]);
    

这样,父组件执行+1操作的时候,子组件也就会自动更新了

  • 流程是这样的
  • 父组件执行+1操作,整个父组件会重新执行
  • UseCallBack监听到 count 发生了变化,会重新创建一个函数,传递给子组件
  • 子组件接受父组件传递的props,传入的函数引用地址和上一次函数的引用地址不一致,那么就会重新执行整个子组件
  • 子组件执行流程:更新组件的事件处理程序 => 生成虚拟DOM树 => 对比前后俩次树结构变化 => 有变化则重新渲染页面;否则不做处理

你以为这就完事了么,不不不,别忘了,js的引用类型数据除了函数还有数组和对象,请看下边的栗子

import { memo, useCallback, useState } from "react";
import ReactDom from "react-dom";

type SubProps = {
  name: string;
  onClick: () => void;
  user: {
    name: string;
    age: number;
  };
};
function SubChildren({ name, onClick }: SubProps) {
  console.log("子组件更新了");
  return <div onClick={onClick}>子组件 {name}</div>;
}

const SubChildrenMemo = memo(SubChildren);

function App() {
  console.log("App父组件");
  const [count, setCount] = useState(0);
  const handleClickSub = useCallback(() => {
    console.log("子组件点击事件");
  }, []);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <SubChildrenMemo
        name="张三"
        user={{ name: "李四", age: 18 }}
        onClick={handleClickSub}
      />
    </div>
  );
}

ReactDom.render(<App />, document.getElementById("root"));

image.png 说明

  • 父组件传递了一个对象给子组件
  • 由于父组件执行+1操作的时候, 父组件更新,都会创建一个新的对象
  • 这个时候,子组件也会重新执行更新操作

React.memo + UseMemo 解决传递对象问题

说明

  • 写法

    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
    
  • 返回一个 memoized 值。

  • 把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

  • 记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

  • 如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

  • 你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。

  • UseMemo 可以记住你想记住的任一类型数据

import { memo, useCallback, useMemo, useState } from "react";
import ReactDom from "react-dom";

type SubProps = {
  name: string;
  onClick: () => void;
  user: {
    name: string;
    age: number;
  };
};
function SubChildren({ name, onClick }: SubProps) {
  console.log("子组件更新了");
  return <div onClick={onClick}>子组件 {name}</div>;
}

const SubChildrenMemo = memo(SubChildren);

function App() {
  console.log("App父组件");
  const [count, setCount] = useState(0);
  const _user = { name: "李四", age: 18 };
  const user = useMemo(() => _user, []);
  const handleClickSub = useCallback(() => {
    console.log("子组件点击事件");
  }, []);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <SubChildrenMemo name="张三" user={user} onClick={handleClickSub} />
    </div>
  );
}

ReactDom.render(<App />, document.getElementById("root"));

image.png 说明

  • 上面的代码中,我们把 user对象信息提升到了顶部,方便使用
  • UseMemo的返回结果传递给了子组件
  • 这样父组件状态改变时,useMemo所依赖的的值没发生改变,当前函数就不会创建一个新的对象返回,子组件也就不会更新
  • useMemo的第二个参数使用方式和 useCallback 的第二个参数一样,都是依赖项,当依赖项发生改变,那么UseMemo这个函数会重新执行,会返回一个新的引用类型的数据,由于子组件接受的props前后俩次值不一致,那么子组件就会被更新

useCallbackUseMemo的区别

  • useCallback第一个参数传入的是一个函数,无返回值
  • UseMemo第一个参数也是一个函数,但这个函数一定要有一个返回值
  • 因为UseMemo有返回值,那么首次执行父组件的时候,当前钩子也会被执行,useCallback钩子在父组件首次加载的时候是不会被执行的

React.memo第二个参数

说明

  • React.memo第二个参数是一个函数,函数中包含俩个参数,分别是上一次的props,和当前父组件传递下来的props
  • React.memo第二个参数 必须有一个布尔类型的返回值,如果返回true那么组件不更新,返回false组件更新,这和shouldComponentUpdate的返回值恰恰相反
  • 本人不建议使用React.memo的第二个参数
    • 有现成的hooks足够我们应对绝大部分情况了,为啥还要手动实现一遍呢

后记

  • 上边我们说UseMemo可以记住任意类型
  • 那把 UseCallBack 改成 UseMemo, 发现功能依旧ok
import { memo, useCallback, useMemo, useState } from "react";
import ReactDom from "react-dom";

type SubProps = {
  name: string;
  onClick: () => void;
  user: {
    name: string;
    age: number;
  };
};
function SubChildren({ name, onClick }: SubProps) {
  console.log("子组件更新了");
  return <div onClick={onClick}>子组件 {name}</div>;
}

const SubChildrenMemo = memo(SubChildren);

function App() {
  console.log("App父组件");
  const [count, setCount] = useState(0);
  const _user = { name: "李四", age: 18 };
  const user = useMemo(() => _user, []);
  const handleClickSub = useMemo(
    () => () => {
      console.log("子组件点击事件");
    },
    []
  );
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <SubChildrenMemo name="张三" user={user} onClick={handleClickSub} />
    </div>
  );
}

ReactDom.render(<App />, document.getElementById("root"));

写在最后

  • 希望大家收获满满