🥳React性能优化三剑客:memo、useMemo、useCallback大揭秘

React 性能那些事儿

家人们,咱搞前端开发的,谁还没被 React 应用的性能问题折磨过呢😫?页面卡顿得像蜗牛爬,加载慢得让人怀疑人生,这些问题是不是一出现就特别影响用户体验,还可能让咱们被产品经理和老板追着问🤣?别担心,今天就给大家带来 React 性能优化的三大法宝:memo、useMemo、useCallback,掌握了它们,性能问题轻松拿捏!

父子组件渲染的 "神秘之旅"

执行顺序:先外后内的探索

在 React 的世界里,父子组件就像一个大家庭,每个成员都有自己的职责和使命。当这个大家庭开始运作,也就是组件开始执行的时候,它遵循着一个有趣的顺序 ------ 先外后内。这就好比你打开一个精美的礼物盒,礼物盒是外层组件,里面的小礼物是内层组件,你肯定得先打开礼物盒,才能看到里面的小礼物,对吧😎?

具体来说,当 React 开始渲染一个组件树时,它会从最外层的父组件开始,逐步深入到内层的子组件。每一个组件在这个过程中,就像是在进行一场接力赛,父组件跑完自己的 "赛程",把接力棒交给子组件,子组件再接着跑。在这个过程中,组件的各种逻辑和代码都会被依次执行,就像你剥洋葱一样,一层一层地解析,直到所有组件都完成了自己的 "初始化" 工作。

渲染完成:先内后外的奥秘

当所有组件都执行完毕,开始完成渲染和挂载时,顺序却来了个大反转,变成了先内后外。这是不是有点奇怪🤔?其实,我们可以把这个过程想象成搭积木,你要先把最底层的小积木搭好,才能往上搭更大的积木,最后完成整个积木塔的搭建。子组件就像是底层的小积木,它们先完成自己的挂载,然后父组件再基于这些已经挂载好的子组件,完成自己的挂载。

这种先内后外的顺序,确保了整个组件树的稳定性和完整性。就好比建房子,你得先把每一层的房间都建好,才能盖上屋顶,不然房子就会塌掉。在 React 中,如果父组件先挂载,而子组件还没准备好,那页面可能就会出现各种奇怪的问题,比如空白、报错等等。所以,React 很聪明地选择了先内后外的挂载顺序,让我们的应用能够稳定、高效地运行。

Button 组件的渲染 "迷思"

局部更新与 Button 的关系

现在,咱们来聚焦一个有趣的问题:Button 组件该不该重新渲染🤔?假设我们有一个父组件,里面有一个计数器count,还有一个Button组件。当count发生改变时,从逻辑上来说,count的变化和Button组件并没有直接关系。就好比你家里客厅的灯开关(父组件的count),和你卧室里的闹钟(Button组件),它们是相互独立的个体。灯开关的变化,不应该影响闹钟的正常运作。

在 React 中,这种情况下Button组件的 JSX 不应该重新渲染。因为重新渲染意味着重新生成虚拟 DOM,进行重绘重排操作。重绘,就像是给房子重新刷漆,只改变房子的外观颜色;而重排则像是重新装修房子,改变房子的布局结构,比如挪动家具的位置、改变房间的大小等。这两个操作都挺耗费性能的,就像你重新刷漆和重新装修房子都需要花费时间和精力一样。如果Button组件每次都因为父组件的一些无关变化而重新渲染,那性能开销可就大了去了,咱们的应用可能就会变得慢吞吞的,用户体验也会直线下降。

性能优化的多面性

为了提升应用的性能,我们可以从多个方面入手。响应式设计是个很重要的点,它能让我们的页面在不同设备上都能完美适配,不管是大屏幕的电脑,还是小屏幕的手机,都能给用户提供良好的视觉体验,就像一件量身定制的衣服,不管谁穿都合身😎。

组件切分也十分关键,把一个大组件拆分成多个小组件,就像把一个大蛋糕切成小块,每个小块都能更好管理。这样做不仅方便复用,还能提升性能。比如在一个电商应用中,把商品列表、购物车、订单详情等功能分别拆分成不同组件,每个组件只负责自己的那部分逻辑,相互独立,互不干扰。当某个组件需要更新时,不会影响其他组件,大大减少了不必要的渲染。

热更新也是提升性能的小能手,它能让我们在不刷新整个页面的情况下,更新部分组件的代码,就像给汽车换零件,不用把整个汽车都换掉,只换有问题的零件就行,这样能快速响应用户的操作,提升用户体验。

组件之间的独立性也不容忽视,就像每个人都有自己独立的生活空间一样,组件之间也应该有清晰的边界,尽量减少相互之间的依赖。这样当一个组件发生变化时,不会对其他组件产生连锁反应。

在子组件中使用React.memo,可以有效防止子组件在 props 没有变化时进行不必要的重新渲染,就像给子组件加了一个 "智能护盾",只有在真正需要的时候才会 "启动"。

还有createContext和useContext,它们可以帮助我们实现组件之间的状态共享。但是,把所有的状态都放到一个Context里可不是个好主意哦😯!因为所有状态都是通过一个reducer生成的,如果这个Context里的状态太多,那么每次状态更新时,可能会导致很多不必要的重新渲染,就像一个大仓库里堆满了东西,每次找一件小物品都要翻遍整个仓库,效率很低。所以,我们要根据实际情况,合理地使用createContext和useContext,把状态分散到合适的Context中,让每个Context都 "轻装上阵"。

jsx 复制代码
import {
  useState,
  useEffect,
  useCallback,
  useMemo, // 缓存一个复杂计算的值
} from "react";
import "./App.css";
import Button from "./components/Button";

function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  console.log("App render");

  // 先定义函数 复杂计算 开销高
  const expensiveComputation = (n) => {
    console.log("expensiveComputation");
    for (let i = 0; i < 1000000; i++) {
      i++;
    }
    return n * 2;
  };

  // 再用 useMemo 缓存计算结果
  const result = useMemo(() => {
    return expensiveComputation(num);
  }, [num]);

  useEffect(() => {
    console.log("count", count);
  }, [count]);
  useEffect(() => {
    console.log("num", num);
  }, [num]);
  // rerender 重新生成
  // 不要重新生成,和 useEffect [] 一样
  //  callback 回调函数 缓存
  const handleClick = useCallback(() => {
    console.log("handleClick");
  }, [num]);

  return (
    <>
      <div>{result}</div>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <br />
      <button onClick={() => setNum(num + 1)}>+</button>
      <br />
      <Button num={num} onClick={handleClick}>
        Love Xiang
      </Button>
    </>
  );
}

export default App;

组件划分的粒度艺术

组件拆分的原则与优势

在 React 的世界里,组件拆分可是一门大学问🧐。就像盖房子一样,我们不能把所有的功能都放在一个大房间里,而是要合理地划分出客厅、卧室、厨房等不同的功能区域。组件拆分也遵循类似的原则,其中单向数据流是一个重要的指导思想。

单向数据流就像是一条单行道路,数据只能从父组件流向子组件。这样做的好处可多啦🤗!首先,它让数据的流动变得非常清晰,就像你在地图上看一条路线,一目了然。当我们调试代码的时候,很容易就能追踪到数据是从哪里来的,又要到哪里去,大大提高了代码的可预测性。比如说,父组件有一个用户信息的状态,它通过 props 把这个信息传递给子组件,子组件只能接收和展示这个信息,不能直接修改。如果子组件需要修改用户信息,它得通过调用父组件传递过来的回调函数,让父组件去更新状态,然后父组件再把新的状态传递给子组件。这样一来,整个数据更新的过程就像一场有序的接力赛,每个组件都清楚自己的职责,不会出现混乱的情况。

除了单向数据流,组件拆分还有很多其他的好处。拆分后的组件具有更好的复用性,就像乐高积木一样,我们可以把一个通用的按钮组件、输入框组件等在不同的页面和功能中重复使用,大大减少了代码的重复编写。而且,每个组件只负责自己的那一小块功能,这使得组件的管理变得更加容易。当我们需要修改某个功能时,只需要找到对应的组件进行修改,不用担心会影响到其他不相关的部分,就像你修理自行车的某个零件,不会影响到整个自行车的其他部件一样。另外,从性能提升的角度来看,组件拆分可以让 React 更精确地控制渲染的范围。当某个组件的状态发生变化时,只有这个组件及其子组件会重新渲染,而不会导致整个页面的所有组件都重新渲染,从而大大提高了渲染效率,让我们的应用更加流畅。

状态更新与组件函数的运行

在 React 中,状态更新是一个非常核心的操作。当组件的状态发生变化时,就好像给组件发出了一个 "重新出发" 的信号🚀,组件函数会重新运行。这是因为 React 采用了一种函数式编程的思想,组件就像是一个函数,输入是 props 和 state,输出是 UI。当输入发生变化时,自然要重新计算输出。

比如说,我们有一个计数器组件,它有一个状态count,初始值为 0。当用户点击按钮时,count的值会增加 1。这个过程中,点击按钮的操作会触发状态更新,React 会重新调用组件函数,在新的函数调用中,count的值已经变成了 1,所以组件渲染出来的 UI 也会相应地更新,显示新的count值。

但是,频繁的状态更新和组件函数重新运行可能会带来性能问题。想象一下,如果一个页面上有很多组件,每个组件都频繁地更新状态,那么整个页面就会不停地重新渲染,就像一个人不停地在做重复的劳动,效率会非常低。这时候,useCallback和React.memo就可以发挥它们的性能优化作用啦😎!

useCallback可以缓存一个回调函数,只有依赖项变化时才重新生成函数实例。我们在前面提到的计数器组件中,如果把点击按钮的回调函数用useCallback包裹起来,当组件重新渲染时,只要依赖项(比如count)没有变化,这个回调函数就不会重新生成。这对于那些传递给子组件的回调函数特别有用,因为如果回调函数每次都重新生成,可能会导致子组件不必要的重新渲染。而React.memo则是用于包裹函数组件,实现组件的 "记忆化"。只有 props 发生变化时,被React.memo包裹的组件才会重新渲染。结合前面的例子,如果我们把显示count值的子组件用React.memo包裹起来,当父组件因为其他原因(比如一个和count无关的状态更新)重新渲染时,只要传递给这个子组件的count值没有变化,子组件就不会重新渲染,从而节省了性能开销。通过useCallback和React.memo的配合使用,我们可以有效地减少不必要的渲染和函数重新生成,让我们的 React 应用性能更上一层楼💪!

React.memo:组件的 "记忆大师"

作用与原理大揭秘

React.memo就像是 React 世界里的 "记忆大师",它是一个高阶组件,专门用来包裹函数组件,给组件赋予神奇的 "记忆化" 能力🧠。怎么理解这个 "记忆化" 呢?想象一下,你有一个特别喜欢的玩具,每次玩完后,你都会记得把它放在哪个位置,下次玩的时候就能直接找到,不用再到处乱找。React.memo对于组件也是这样,它会仔细对比前后的 props。当新的 props 和之前的 props 一样时,它就会说:"嘿,没啥变化嘛,不用重新渲染啦,直接用上次的就行!" 这样就跳过了渲染过程,直接复用上一次的渲染结果,大大提升了性能。就好比你每次出门都要换衣服,如果衣服没脏没破,你就不用再花时间去挑新衣服换,直接穿着上次的出门,节省了不少时间和精力,React 应用的性能也在这个过程中得到了提升。

适用场景与示例展示

在实际开发中,React.memo有很多用武之地。比如说,当你的组件依赖的 props 很少变化时,它就能大显身手。假设你有一个展示用户基本信息的组件,像用户名、头像这些信息,一旦用户登录后就很少会改变。这时候,把这个组件用React.memo包裹起来,就算父组件因为其他原因频繁更新,只要传递给这个用户信息展示组件的 props(用户名、头像等)没有变化,它就不会重新渲染,是不是很省心😎?

再比如,父组件频繁更新,但子组件无需每次都渲染的情况。想象一个电商页面,父组件负责管理整个页面的各种状态,像购物车的数量、筛选条件等,它可能会因为用户的各种操作频繁更新。而子组件是一个商品详情展示组件,只要用户没有切换商品,它所展示的商品信息(也就是 props)就不会变。这时候,用React.memo把商品详情展示组件包裹起来,就能避免它因为父组件的频繁更新而不必要地重新渲染,提升页面的加载速度和流畅度。

下面来看一个具体的代码示例:

jsx 复制代码
import { memo } from "react";
const Button = ({ num }) => <button>{num}</button>;
export default memo(Button);

在这个例子中,Button组件接收一个num的 props。当父组件更新时,如果传递给Button组件的num没有变化,那么Button组件就不会重新渲染,而是直接复用之前的渲染结果,这样就减少了不必要的性能开销。

注意事项需牢记

虽然React.memo很强大,但使用的时候也有一些注意事项⚠️。它只对 props 进行浅比较,这意味着它只会比较 props 的第一层(顶层)值。如果 props 是复杂的数据类型,比如对象或数组,并且这个对象 / 数组的引用发生了变化,即使其内容没有变,React.memo也会认为 props 已经改变,从而会重新渲染组件。

举个例子,假设有一个组件接收一个对象person作为 props,里面包含name和age属性。如果我们这样写代码:

jsx 复制代码
import { memo } from "react";
const PersonComponent = ({ person }) => (
  <div>
    <p>Name: {person.name}</p>
    <p>Age: {person.age}</p>
  </div>
);
const MemoizedPersonComponent = memo(PersonComponent);
const App = () => {
  const [person, setPerson] = useState({ name: 'John', age: 30 });
  const handleClick = () => {
    // 这里虽然对象内容没变,但引用变了
    setPerson({...person }); 
  };
  return (
    <div>
      <MemoizedPersonComponent person={person} />
      <button onClick={handleClick}>Click me</button>
    </div>
  );
};

当点击按钮时,虽然person对象的内容没有变化,但setPerson({...person })创建了一个新的对象引用,React.memo会认为 props 发生了变化,从而导致MemoizedPersonComponent重新渲染。为了避免这种情况,当 props 包含复杂对象时,我们需要自定义对比函数。可以这样修改代码:

jsx 复制代码
import { memo } from "react";
const PersonComponent = ({ person }) => (
  <div>
    <p>Name: {person.name}</p>
    <p>Age: {person.age}</p>
  </div>
);
const areEqual = (prevProps, nextProps) => {
  return prevProps.person.name === nextProps.person.name && prevProps.person.age === nextProps.person.age;
};
const MemoizedPersonComponent = memo(PersonComponent, areEqual);
const App = () => {
  const [person, setPerson] = useState({ name: 'John', age: 30 });
  const handleClick = () => {
    setPerson({...person }); 
  };
  return (
    <div>
      <MemoizedPersonComponent person={person} />
      <button onClick={handleClick}>Click me</button>
    </div>
  );
};

通过自定义的areEqual函数,我们精确地比较了person对象的name和age属性,只有当这两个属性真正发生变化时,MemoizedPersonComponent才会重新渲染,这样就避免了因引用变化导致的不必要渲染,让我们的组件性能更加稳定和高效。

useMemo:计算结果的 "缓存神器"

作用与原理剖析

useMemo就像是 React 送给我们的一个 "缓存神器",专门用来对付那些计算开销较大的任务。在 React 组件的渲染过程中,有时候我们会遇到一些复杂的计算,比如对一个庞大的数组进行排序、过滤,或者进行复杂的数学运算。这些计算就像是一场 "马拉松",需要耗费大量的时间和精力。如果每次组件渲染时都要重新进行这些计算,那我们的应用性能肯定会受到很大的影响,就像一辆车总是在不断地进行高负荷运转,迟早会 "罢工" 的😫。

useMemo的出现,就解决了这个大难题。它的作用就是缓存一个计算开销较大的值,只有当依赖项发生变化时,才会重新计算。这就好比你有一个超级复杂的拼图,每次拼好都要花费很长时间。如果下次需要这个拼图的时候,发现它已经被缓存起来了,直接拿出来用就好,是不是超级方便😎?useMemo的原理其实也不难理解,它依赖于一个依赖数组。当组件首次渲染时,useMemo会执行传入的函数,并将计算结果缓存起来。当组件再次渲染时,useMemo会检查依赖数组中的值是否发生了变化。如果依赖项没有变化,它就会直接返回上一次缓存的值,避免了重复计算;如果依赖项发生了变化,它才会重新执行函数,计算新的值并缓存起来。

适用场景与示例呈现

useMemo在很多场景中都能大显身手,尤其是在处理复杂计算和数据处理的时候。比如说,在一个电商应用中,我们需要计算购物车中所有商品的总价,这个计算过程可能涉及到遍历商品列表,获取每个商品的价格和数量,然后进行累加。如果每次组件渲染都重新计算这个总价,那性能开销可就大了。这时候,useMemo就可以派上用场啦。

jsx 复制代码
import React, { useState, useMemo } from "react";
const Cart = () => {
  const [items, setItems] = useState([
    { id: 1, name: "商品1", price: 10, quantity: 2 },
    { id: 2, name: "商品2", price: 15, quantity: 3 },
  ]);
  const totalPrice = useMemo(() => {
    return items.reduce((total, item) => total + item.price * item.quantity, 0);
  }, [items]);
  const addItem = () => {
    setItems([...items, { id: 3, name: "商品3", price: 20, quantity: 1 }]);
  };
  return (
    <div>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.name} - 价格: {item.price} - 数量: {item.quantity}
          </li>
        ))}
      </ul>
      <p>总价: {totalPrice}</p>
      <button onClick={addItem}>添加商品</button>
    </div>
  );
};
export default Cart;

在这个例子中,totalPrice是通过useMemo计算并缓存的。只有当items数组发生变化时,totalPrice才会重新计算。这样,当我们点击 "添加商品" 按钮时,虽然组件会重新渲染,但totalPrice不会每次都重新计算,大大提高了性能。

再比如,在一个图表展示组件中,我们需要对大量的数据进行处理,生成适合图表展示的数据格式。这个数据处理过程可能非常复杂,而且计算量很大。如果每次组件渲染都重新进行数据处理,那图表的加载速度会非常慢。使用useMemo,我们可以将处理后的数据缓存起来,只有当原始数据发生变化时,才重新处理数据。这样可以显著提高图表的加载速度,给用户带来更好的体验。

注意事项别忽视

虽然useMemo很强大,但使用的时候也有一些注意事项,其中最关键的就是依赖项要写全⚠️。如果依赖项没有写全,可能会出现 "脏数据" 的问题。比如说,我们在上面的购物车例子中,如果totalPrice的计算还依赖于一个折扣率discount,但我们在useMemo的依赖数组中没有包含discount,那么当discount发生变化时,totalPrice不会重新计算,就会导致显示的总价不是最新的,这就会给用户带来困惑,也会影响应用的正确性和性能。

jsx 复制代码
import React, { useState, useMemo } from "react";
const Cart = () => {
  const [items, setItems] = useState([
    { id: 1, name: "商品1", price: 10, quantity: 2 },
    { id: 2, name: "商品2", price: 15, quantity: 3 },
  ]);
  const [discount, setDiscount] = useState(0.9);
  // 错误示范:依赖数组中没有包含discount
  const totalPrice = useMemo(() => {
    return items.reduce((total, item) => total + item.price * item.quantity, 0) * discount;
  }, [items]);
  const addItem = () => {
    setItems([...items, { id: 3, name: "商品3", price: 20, quantity: 1 }]);
  };
  const changeDiscount = () => {
    setDiscount(0.8);
  };
  return (
    <div>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.name} - 价格: {item.price} - 数量: {item.quantity}
          </li>
        ))}
      </ul>
      <p>总价: {totalPrice}</p>
      <button onClick={addItem}>添加商品</button>
      <button onClick={changeDiscount}>修改折扣</button>
    </div>
  );
};
export default Cart;

在这个错误示范中,当我们点击 "修改折扣" 按钮时,discount的值发生了变化,但由于useMemo的依赖数组中没有包含discount,totalPrice不会重新计算,显示的总价还是按照原来的折扣计算的,这显然是不正确的。所以,在使用useMemo时,一定要仔细检查依赖数组,确保包含了所有影响计算结果的变量,这样才能让useMemo发挥出它最大的威力,为我们的应用性能保驾护航💪!

useCallback:回调函数的 "稳定器"

作用与原理探究

useCallback就像是 React 应用中的 "稳定器",专门用来处理回调函数的稳定性问题。在 React 的世界里,函数就像是一个个小助手,每个小助手都有自己的职责。而回调函数呢,就像是被派出去执行特定任务的小助手,当某个事件发生时,它就会被调用。

在组件的渲染过程中,每次组件重新渲染,函数都会重新创建,就好像每次都要重新招聘一批小助手,即使这些小助手的工作内容并没有改变。这不仅浪费资源,还可能会引发一些问题。比如,当我们把一个回调函数传递给子组件时,如果这个回调函数每次都重新创建,子组件可能会误以为这是一个全新的任务,从而进行不必要的重新渲染。

useCallback的作用就是缓存一个回调函数,让这个小助手能够稳定地工作。它只有在依赖项发生变化时,才会重新生成函数实例。这就好比给小助手安排了一个固定的岗位,只要工作环境(依赖项)没有变化,小助手就可以一直在这个岗位上高效地工作,不需要重新招聘和培训。

它的原理也不难理解,useCallback依赖于一个依赖数组。当组件首次渲染时,它会将传入的回调函数缓存起来。当组件再次渲染时,它会检查依赖数组中的值是否发生了变化。如果依赖项没有变化,它就会返回上一次缓存的函数引用,就像从 "小助手储备库" 中直接拿出上次的小助手继续工作;如果依赖项发生了变化,它才会重新生成一个新的回调函数并缓存起来,就像重新招聘一个更适合新环境的小助手。

适用场景与示例解读

useCallback在很多场景中都能发挥重要作用,其中一个典型的适用场景就是当我们需要将回调函数传递给用React.memo包裹的子组件时。我们前面提到过,React.memo可以防止子组件在 props 没有变化时重新渲染。但是,如果我们传递给子组件的回调函数每次都重新创建,即使 props 中的其他数据没有变化,React.memo也会认为 props 发生了变化,从而导致子组件重新渲染。这就好比我们给子组件送礼物(props),虽然礼物的大部分内容都没变,但其中一个小礼物(回调函数)每次都换了新包装,子组件就会觉得收到了全新的礼物,忍不住要重新 "打量" 一番(重新渲染)。

这时候,useCallback就可以派上用场了。我们看一个实际的代码示例:

jsx 复制代码
import React, { useState, useCallback } from "react";
const ChildComponent = React.memo(({ onClick }) => {
  console.log("ChildComponent rendered");
  return <button onClick={onClick}>Click me</button>;
});
const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);
  return (
    <div>
      <ChildComponent onClick={handleClick} />
      <p>Count: {count}</p>
    </div>
  );
};
export default ParentComponent;

在这个例子中,ParentComponent是父组件,ChildComponent是用React.memo包裹的子组件。ParentComponent中有一个状态count和一个点击按钮的回调函数handleClick。handleClick函数使用useCallback进行了缓存,并且依赖于count。当我们点击按钮时,count的值会增加,handleClick函数也会因为count的变化而重新生成。但是,在其他情况下,比如父组件因为其他无关状态的更新而重新渲染时,只要count没有变化,handleClick函数就不会重新生成,这样就保证了传递给ChildComponent的回调函数的稳定性,从而避免了ChildComponent不必要的重新渲染。你可以在浏览器的控制台中看到,只有当count变化时,ChildComponent才会重新渲染,而不是每次父组件重新渲染它都跟着渲染,是不是很神奇😎?

再比如,在一个列表组件中,每个列表项都有一个点击事件的回调函数。如果我们不使用useCallback,每次列表组件重新渲染,每个列表项的回调函数都会重新创建,这会导致不必要的性能开销。而使用useCallback,我们可以将每个列表项的回调函数缓存起来,只有当相关的依赖项(比如列表数据、点击处理逻辑依赖的其他状态等)发生变化时,回调函数才会重新生成,大大提高了列表组件的性能和稳定性。

注意事项要谨记

使用useCallback时,有一个非常重要的注意事项,那就是依赖项一定要写全⚠️。如果依赖项没有写全,就像是给小助手安排工作时,没有把所有需要的工具和信息都告诉他,可能会导致闭包内变量不是最新值,从而引发各种逻辑错误和性能问题。

举个例子,我们还是以上面的ParentComponent为例,如果我们不小心把handleClick函数的依赖数组写成了空数组[],就像这样:

jsx 复制代码
const handleClick = useCallback(() => {
  setCount(count + 1);
}, []);

这时候,handleClick函数就不会依赖于count的变化,无论count怎么变,handleClick函数都不会重新生成。当我们点击按钮时,setCount(count + 1)中的count永远是组件首次渲染时的初始值,因为handleClick函数所在的闭包中的count没有随着组件的重新渲染而更新,这显然不是我们想要的结果。

又比如,我们的回调函数依赖于多个状态,假设handleClick函数不仅要更新count,还要根据另一个状态isEnabled来决定是否执行某些操作,代码如下:

jsx 复制代码
const [count, setCount] = useState(0);
const [isEnabled, setIsEnabled] = useState(true);
const handleClick = useCallback(() => {
  if (isEnabled) {
    setCount(count + 1);
  }
}, [count]);

在这个例子中,我们只把count写进了依赖数组,而忽略了isEnabled。当isEnabled发生变化时,handleClick函数不会重新生成,这就可能导致当isEnabled变为false时,点击按钮仍然会执行setCount(count + 1)的操作,因为handleClick函数中的isEnabled还是旧的值,这就会产生逻辑错误。所以,在使用useCallback时,一定要仔细检查依赖数组,确保包含了所有影响回调函数逻辑的变量,这样才能让useCallback真正发挥它的作用,为我们的 React 应用保驾护航💪!

三剑客的联手出击:结合使用场景

父组件与子组件的优化协作

在实际的 React 应用开发中,为了实现最大化的性能优化,我们常常需要将memo、useMemo和useCallback这三个法宝结合起来使用。就像组建一支超级战队,每个成员都发挥自己的独特技能,共同战胜性能问题这个大怪兽👹。

假设我们正在开发一个电商购物车功能。父组件负责管理购物车中的商品列表、计算总价以及处理用户的操作,比如添加商品、删除商品等。子组件则负责展示单个商品的信息和操作按钮,比如商品图片、名称、价格、数量,以及 "删除" 按钮。

首先,在父组件中,我们会有一些复杂的计算,比如计算购物车中所有商品的总价。这个计算可能涉及到遍历商品列表,根据每个商品的价格和数量进行累加。如果每次组件渲染都重新计算这个总价,那性能开销可就大了。这时候,useMemo就可以大显身手啦。

jsx 复制代码
import React, { useState, useMemo } from "react";
const Cart = () => {
  const [items, setItems] = useState([
    { id: 1, name: "商品1", price: 10, quantity: 2 },
    { id: 2, name: "商品2", price: 15, quantity: 3 },
  ]);
  const totalPrice = useMemo(() => {
    return items.reduce((total, item) => total + item.price * item.quantity, 0);
  }, [items]);
  const addItem = () => {
    setItems([...items, { id: 3, name: "商品3", price: 20, quantity: 1 }]);
  };
  return (
    <div>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.name} - 价格: {item.price} - 数量: {item.quantity}
          </li>
        ))}
      </ul>
      <p>总价: {totalPrice}</p>
      <button onClick={addItem}>添加商品</button>
    </div>
  );
};
export default Cart;

在这段代码中,totalPrice是通过useMemo计算并缓存的。只有当items数组发生变化时,totalPrice才会重新计算。这样,当我们进行一些其他操作,比如点击 "添加商品" 按钮导致组件重新渲染时,如果items数组没有实质性的改变(比如只是触发了一些与商品列表无关的状态更新),totalPrice就不会重新计算,大大提高了性能。

接下来,我们考虑处理用户操作的回调函数。比如,当用户点击 "删除" 按钮时,我们需要从购物车中移除对应的商品。这个删除操作的回调函数可能会被传递给子组件中的 "删除" 按钮。如果每次父组件渲染时这个回调函数都重新创建,可能会导致子组件不必要的重新渲染。这时候,useCallback就派上用场了。

jsx 复制代码
import React, { useState, useCallback } from "react";
const Cart = () => {
  const [items, setItems] = useState([
    { id: 1, name: "商品1", price: 10, quantity: 2 },
    { id: 2, name: "商品2", price: 15, quantity: 3 },
  ]);
  const totalPrice = useMemo(() => {
    return items.reduce((total, item) => total + item.price * item.quantity, 0);
  }, [items]);
  const addItem = () => {
    setItems([...items, { id: 3, name: "商品3", price: 20, quantity: 1 }]);
  };
  const removeItem = useCallback((itemId) => {
    setItems(items.filter((item) => item.id!== itemId));
  }, [items]);
  return (
    <div>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.name} - 价格: {item.price} - 数量: {item.quantity}
            <button onClick={() => removeItem(item.id)}>删除</button>
          </li>
        ))}
      </ul>
      <p>总价: {totalPrice}</p>
      <button onClick={addItem}>添加商品</button>
    </div>
  );
};
export default Cart;

在这个例子中,removeItem函数使用useCallback进行了缓存,并且依赖于items。这样,只要items数组没有变化,removeItem函数就不会重新创建,保证了传递给子组件的回调函数的稳定性,避免了子组件因为回调函数的变化而不必要地重新渲染。

最后,我们来看子组件。子组件只负责展示单个商品的信息和操作按钮,它的渲染应该尽量高效。我们可以使用React.memo来包裹子组件,只有当传递给它的 props 发生变化时,它才会重新渲染。

jsx 复制代码
import React from "react";
const ItemComponent = React.memo(({ item, onRemove }) => {
  return (
    <li>
      {item.name} - 价格: {item.price} - 数量: {item.quantity}
      <button onClick={() => onRemove(item.id)}>删除</button>
    </li>
  );
});
export default ItemComponent;

在这段代码中,ItemComponent是一个展示单个商品信息的子组件,它被React.memo包裹。只有当item(商品信息)或onRemove(删除回调函数)发生变化时,ItemComponent才会重新渲染。由于我们在父组件中使用了useMemo和useCallback来确保item和onRemove在不必要时不会变化,所以ItemComponent的渲染次数大大减少,从而提升了整个购物车功能的性能。

实际案例深入分析

为了更深入地理解在不同场景下如何灵活运用memo、useMemo、useCallback,我们来看一个稍微复杂一点的 React 应用案例 ------ 一个图片列表展示与筛选功能。

假设我们有一个图片库应用,页面上展示了一系列图片,并且提供了筛选功能,用户可以根据图片的类别、尺寸等条件进行筛选。

首先,我们有一个父组件ImageGallery,它负责管理图片数据、筛选条件以及处理筛选操作。图片数据可能是从后端接口获取的,并且数据量较大。

jsx 复制代码
import React, { useState, useMemo, useCallback } from "react";
import ImageItem from "./ImageItem";
const ImageGallery = () => {
  const [images, setImages] = useState([]);
  const [filterCategory, setFilterCategory] = useState("all");
  const [filterSize, setFilterSize] = useState("all");
  // 模拟从后端获取图片数据
  useEffect(() => {
    const fetchImages = async () => {
      // 这里省略实际的网络请求代码,假设返回一个图片数组
      const data = await getImagesFromServer();
      setImages(data);
    };
    fetchImages();
  }, []);
  // 使用useMemo根据筛选条件过滤图片
  const filteredImages = useMemo(() => {
    return images.filter((image) => {
      const categoryMatch = filterCategory === "all" || image.category === filterCategory;
      const sizeMatch = filterSize === "all" || image.size === filterSize;
      return categoryMatch && sizeMatch;
    });
  }, [images, filterCategory, filterSize]);
  // 使用useCallback处理筛选条件的变化
  const handleCategoryChange = useCallback((e) => {
    setFilterCategory(e.target.value);
  }, []);
  const handleSizeChange = useCallback((e) => {
    setFilterSize(e.target.value);
  }, []);
  return (
    <div>
      <select onChange={handleCategoryChange}>
        <option value="all">所有类别</option>
        <option value="风景">风景</option>
        <option value="人物">人物</option>
      </select>
      <select onChange={handleSizeChange}>
        <option value="all">所有尺寸</option>
        <option value="大">大</option>
        <option value="中">中</option>
        <option value="小">小</option>
      </select>
      <ul>
        {filteredImages.map((image) => (
          <ImageItem key={image.id} image={image} />
        ))}
      </ul>
    </div>
  );
};
export default ImageGallery;

在这个父组件中,useMemo用于根据筛选条件过滤图片。只有当images(图片数据)、filterCategory(筛选类别)或filterSize(筛选尺寸)发生变化时,filteredImages才会重新计算。这样,当用户进行其他操作,比如点击页面上的其他按钮,而筛选条件没有改变时,图片过滤的操作就不会重复执行,提高了性能。

useCallback用于处理筛选条件的变化。handleCategoryChange和handleSizeChange函数被useCallback缓存,只有当依赖项发生变化时(这里依赖项为空数组,即不会因为父组件的普通重新渲染而重新创建),函数才会重新生成。这样,当父组件因为其他原因重新渲染时,传递给select元素的onChange回调函数不会改变,避免了不必要的重新渲染。

接下来,我们看子组件ImageItem,它负责展示单个图片。

jsx 复制代码
import React from "react";
const ImageItem = React.memo(({ image }) => {
  return (
    <li>
      <img src={image.url} alt={image.title} />
      <p>{image.title}</p>
      <p>类别: {image.category}</p>
      <p>尺寸: {image.size}</p>
    </li>
  );
});
export default ImageItem;

ImageItem组件被React.memo包裹,只有当传递给它的image属性发生变化时,它才会重新渲染。由于父组件中使用了useMemo来确保filteredImages在不必要时不会变化,所以ImageItem的渲染次数也会大大减少。

通过这个案例,我们可以看到,在一个实际的 React 应用中,memo、useMemo和useCallback是如何协同工作,解决性能问题,提升应用性能的。在不同的场景下,我们要根据具体的需求和数据变化情况,灵活运用这三个优化工具,让我们的 React 应用更加高效、流畅地运行💪!

总结:性能优化的关键法宝

到这里,我们已经深入了解了 React 性能优化中的三大法宝:memo、useMemo和useCallback。它们各自有着独特的技能,在不同的场景下发挥着重要作用。

React.memo就像一个 "智能守卫",站在组件的入口,仔细检查 props 是否有变化。只有当 props 发生改变时,才允许组件重新渲染,大大减少了不必要的渲染开销,让组件的渲染更加高效。

useMemo则是计算结果的 "缓存大师",当遇到复杂的计算任务时,它会将计算结果缓存起来。只有依赖项发生变化,才会重新计算,避免了重复计算带来的性能损耗,让我们的应用在处理复杂数据时也能游刃有余。

useCallback是回调函数的 "稳定器",它能缓存回调函数,确保在依赖项不变的情况下,回调函数的引用保持稳定。这对于那些传递给子组件的回调函数非常重要,避免了因回调函数的变化导致子组件不必要的重新渲染。

在实际项目中,我们要根据具体的业务场景和性能需求,灵活运用这三个工具。比如在电商应用中,计算购物车总价、处理商品列表的筛选和排序等场景,都可以通过它们来优化性能。当我们开发一个复杂的表单组件,包含各种输入框、下拉框和按钮时,也可以利用它们来提升组件的性能和用户体验。

希望大家在今后的 React 开发中,能够熟练运用memo、useMemo和useCallback,让我们的 React 应用跑得更快、更稳!如果在使用过程中有任何问题或者心得,欢迎在评论区留言分享哦💬!

相关推荐
if时光重来6 分钟前
axios统一封装规范管理
前端·vue.js
m0dw14 分钟前
js迭代器
开发语言·前端·javascript
烛阴18 分钟前
别再让 JavaScript 卡死页面!Web Workers 零基础上手指南
前端·javascript
tianzhiyi1989sq22 分钟前
Vue项目中的AJAX请求与跨域问题解析
前端·vue.js·ajax
结城25 分钟前
Vue 3 响应式系统中的 effectScope、watchEffect、effect 和 watch 详解
前端·javascript·vue.js
90后的晨仔33 分钟前
🚀 零构建!教你通过 CDN 快速使用 Vue 3(含模块拆分 + Import Maps 实战)
前端·vue.js
超级土豆粉1 小时前
Taro 本地存储 API 详解与实用指南
前端·javascript·react.js·taro
wordbaby1 小时前
别再用错了!一分钟让你区分 useRef 和 useState
前端·react.js
前端一小卒1 小时前
万字长文带你从零理解React Server Components
前端·javascript·react.js
90后的晨仔1 小时前
📦 npm、yarn、pnpm、bun 是什么?有什么区别?哪个更适合你?【全面对比指南】
前端