让 React “超赛”!了解一下 useMemo、useCallback 和 React.memo

236 阅读10分钟

前言:

好久没有写博客了,这期间又是反复失业,又生了自己可爱的闺女,又开心又焦虑

如果你觉得组件渲染慢得像“撒旦”(龙珠反派),是时候用上 React 的三大性能优化神器了:useMemouseCallbackReact.memo。想象一下,这就像让你的网站“超赛变身”,优化得好,加载速度就能“咻”地提升好几倍!不过,使用不当可能会适得其反。今天,我们来好好“修炼”这三大技能,助你的应用达到“超级赛亚人”级别!


一、useMemo:让组件“大脑”不再发烧

useMemo 的作用:记住计算结果,省时省力

在 React 中,useMemo 可以缓存一些复杂计算的结果,避免每次渲染都重新计算。适合用在那些消耗较大且不需要每次重新计算的场景中,比如复杂的战斗力计算或招数提升。

示例:孙悟空的战斗力计算

想象一下,你有一个组件要实时计算孙悟空的战斗力。悟空的战斗力会在他“濒死复活”、遇到更厉害的敌人,或者习得新的招数时提升。但每次渲染组件时,如果没有必要重新计算战斗力,就有点浪费资源了。让我们用 useMemo 来“记住”结果,只有当战斗力需要提升时才重新计算。

使用前(未优化):

// BattlePower 组件负责显示悟空的战斗力
function BattlePower({ enemy, technique }) {
  // 每次渲染都重新计算战斗力
  const powerLevel = enemy.power + technique.powerBoost;
  return <div>悟空的战斗力:{powerLevel}</div>;
}
export default BattlePower;

代码解析:

• enemy:当前敌人对象,每个敌人有一个 power 属性,表示其战斗力。例如,弗利萨的战斗力是 10,沙鲁是 100。

• technique:悟空学会的招数对象,每个招数有一个 powerBoost 属性,表示对战斗力的提升。

• powerLevel:通过累加敌人的战斗力和招数的战斗力提升来计算悟空的总战斗力。

• 每次 BattlePower 组件渲染时,无论 enemy 或 technique 是否变化,都会重新计算 powerLevel,这在数据量大时会影响性能。

使用后(正确使用):

import { useMemo } from 'react';
// BattlePower 组件负责显示悟空的战斗力
function BattlePower({ enemy, technique }) {
  // 使用 useMemo 缓存计算结果,只有当 enemy 或 technique 变化时才重新计算
  const powerLevel = useMemo(() => {
    console.log("计算战斗力...");
    const enemiesPower = enemy.power; // 当前敌人的战斗力
    const techniquesPower = technique.powerBoost; // 当前招数的战斗力提升
    return enemiesPower + techniquesPower; // 总战斗力
  }, [enemy, technique]); // 依赖数组,只有当 enemy 或 technique 变化时才重新计算
  return <div>悟空的战斗力:{powerLevel}</div>;
}

export default BattlePower;

代码解析:

• useMemo:缓存了 powerLevel 的计算过程。

• 只有当 enemy 或 technique 发生变化时,useMemo 才会重新执行计算函数,更新 powerLevel。

• 如果组件多次渲染但 enemy 和 technique 没有变化,powerLevel 的计算将被跳过,提升性能。

错误使用示例:

如果只是简单的加法运算,使用 useMemo 反而是浪费资源。

const simpleSum = useMemo(() => 1 + 1, []); // 错误使用:简单运算不需要 useMemo

总结:

useMemo 适用于“烧脑”的复杂计算,减少不必要的重复计算。对于简单的运算或依赖频繁变化的场景,过度使用会让代码变“累赘”。

二、useCallback:让回调函数变“龟派气功”

useCallback 的作用:记住回调函数,减少不必要的重渲染

useCallback 的作用是缓存回调函数,确保函数引用在依赖不变时保持不变。如果你的回调函数传递给子组件,每次渲染生成新函数可能会让子组件频繁重渲染。

示例:悟空的龟派气功

假设我们有一个“悟空”组件,包含了龟派气功的回调函数,每次点击都在子组件中触发,但每次渲染都生成新的引用,会浪费不少气力。我们用 useCallback 来优化!

使用前(未优化):

// GokuBattle 组件负责管理悟空的战斗力
function GokuBattle() {
  const [powerLevel, setPowerLevel] = useState(9000); // 悟空的战斗力状态
  // 每次渲染都生成新的回调函数
  const doKamehameha = () => setPowerLevel(prev => prev + 1000); // 龟派气功:增加战斗力
  return <Goku onKamehameha={doKamehameha} />; // 将回调函数传递给悟空组件
}

// Goku 组件负责触发龟派气功
function Goku({ onKamehameha }) {
  console.log("Goku 渲染");
  return <button onClick={onKamehameha}>龟派气功</button>; // 按钮点击触发龟派气功
}

export default GokuBattle;

代码解析:

• GokuBattle:维护了 powerLevel 状态,表示悟空的战斗力。

• doKamehameha:一个回调函数,用于增加 powerLevel。

• 每次 GokuBattle 组件渲染时,doKamehameha 函数都会重新生成一个新引用。

• Goku:接收 onKamehameha 作为 props,每次 GokuBattle 渲染时都会触发 Goku 的重新渲染。

使用后(正确使用):

import React, { useState, useCallback } from 'react';
// GokuBattle 组件负责管理悟空的战斗力
function GokuBattle() {
  const [powerLevel, setPowerLevel] = useState(9000); // 悟空的战斗力状态
  // 使用 useCallback 缓存回调函数,依赖项为空数组表示不依赖任何外部变量
  const doKamehameha = useCallback(() => {
    setPowerLevel(prev => prev + 1000); // 龟派气功:增加战斗力
  }, []); // 仅在组件挂载时创建一次
  return <Goku onKamehameha={doKamehameha} />; // 将缓存的回调函数传递给悟空组件
}

// Goku 组件负责触发龟派气功
function Goku({ onKamehameha }) {
  console.log("Goku 渲染");
  return <button onClick={onKamehameha}>龟派气功</button>; // 按钮点击触发龟派气功
}

export default GokuBattle;

代码解析:

• useCallback:缓存了 doKamehameha 回调函数,使其在组件生命周期内保持同一个引用。

• 只有当 doKamehameha 的依赖项(此例中为空数组,即无依赖)发生变化时,才会重新生成新的函数引用。

• 这样,Goku 组件在 GokuBattle 重新渲染时不会因为 onKamehameha 的引用变化而重新渲染。

错误使用示例:

没有必要缓存的简单函数,过度使用 useCallback 反而让代码更复杂。

const simpleLog = useCallback(() => console.log("龟派气功!"), []); // 错误使用:简单的 console.log 不需要缓存

总结:

useCallback 更适合传递给子组件的复杂回调,减少子组件不必要的重渲染。简单函数或不频繁更新的函数可以直接写。

三、React.memo:让组件“记住”渲染结果

React.memo 的作用:避免子组件不必要的重渲染

React.memo 是一个高阶组件,用于记住子组件的渲染结果。如果子组件的 props 没有变化,则不会触发重新渲染。这就像是给组件加上“超级记忆”,节省渲染时间,特别适合渲染代价较高的组件。

示例:赛亚人防护罩

假设有一个“赛亚人护罩”组件,如果父组件频繁更新但护罩不变,我们可以用 React.memo 给它加上“记忆防护”,避免无谓的重渲染。

使用前(未优化):

// SaiyanShield 组件负责显示护罩等级
function SaiyanShield({ shieldLevel }) {
  console.log("渲染护罩");
  return <div>护罩等级:{shieldLevel}</div>; // 显示护罩等级
}

export default SaiyanShield;

代码解析:

• SaiyanShield:接收 shieldLevel 作为 props,并显示护罩等级。

• 每次父组件渲染时,无论 shieldLevel 是否变化,SaiyanShield 都会重新渲染,打印“渲染护罩”。

使用后(正确使用):

import { memo } from 'react';
// 使用 React.memo 包装组件,只有 shieldLevel 变化时才重新渲染
const SaiyanShield = memo(({ shieldLevel }) => {
  console.log("渲染护罩");
  return <div>护罩等级:{shieldLevel}</div>; // 显示护罩等级
});

export default SaiyanShield;

代码解析:

• React.memo:包裹了 SaiyanShield 组件,使其在 shieldLevel 不变时不会重新渲染。

• 只有当 shieldLevel 发生变化时,SaiyanShield 才会重新渲染,打印“渲染护罩”。

错误使用示例:

对于简单、渲染负担低的组件,React.memo 反而增加了额外检查的负担。

const SimpleButton = React.memo(({ onClick }) => <button onClick={onClick}>按我!</button>); // 错误使用:小组件不适合用 React.memo

总结:

React.memo 是组件级别的缓存,适合高渲染代价的组件。简单组件不需要缓存,以免徒增性能检查负担。

四、三者的最佳组合——“终极元气弹”

在大型应用中,这三者常常需要协同发挥“元气弹”般的威力。来看一个“终极战斗力”的示例。

import React, { useState, useCallback, useMemo, memo } from 'react';
// EnemyList 组件负责显示敌人信息,并允许攻击敌人
const EnemyList = memo(({ enemy, onAttack }) => {
  console.log("渲染敌人列表");
  return (
    <div>
      <h3>敌人:{enemy.name}</h3>
      <p>战斗力:{enemy.power}</p>
      <button onClick={() => onAttack(enemy)}>攻击</button> {/* 点击按钮触发攻击 */}
    </div>
  );
});

// GokuBattle 组件负责管理悟空的战斗力和当前敌人
function GokuBattle() {
  const [currentEnemy, setCurrentEnemy] = useState({ name: "弗利萨", power: 10 }); // 当前敌人状态
  const [technique, setTechnique] = useState({ name: "龟派气功", powerBoost: 5 }); // 当前招数状态
  // 使用 useMemo 缓存战斗力的计算,只有当 currentEnemy 或 technique 变化时才重新计算
  const powerLevel = useMemo(() => {
    console.log("计算战斗力...");
    const enemiesPower = currentEnemy.power; // 当前敌人的战斗力
    const techniquesPower = technique.powerBoost; // 当前招数的战斗力提升
    return enemiesPower + techniquesPower; // 总战斗力
  }, [currentEnemy, technique]);
    // 使用 useCallback 缓存攻击函数,避免每次渲染都生成新函数引用

  const handleAttack = useCallback((enemy) => {
    console.log(`攻击 ${enemy.name}!`);
    // 攻击逻辑:减少敌人的战斗力
    setCurrentEnemy(prevEnemy => ({
      ...prevEnemy,
      power: prevEnemy.power - 5, // 每次攻击减少5点战斗力
    }));
  }, []); // 依赖项为空数组,表示函数引用在组件生命周期内保持不变

  return (
    <div>
      <h2>悟空的战斗力:{powerLevel}</h2>
      {/* 渲染当前敌人列表,并传递攻击函数 */}
      <EnemyList enemy={currentEnemy} onAttack={handleAttack} />
      {/* 按钮用于悟空习得新招数 */}
      <button onClick={() => setTechnique({ name: "元气弹", powerBoost: 20 })}>
        学习新招数:元气弹
      </button>
    </div>
  );
}

export default GokuBattle;

代码解析:

EnemyList组件:

• 使用 React.memo 包装,只有当 enemyonAttack 发生变化时才重新渲染。

• 接收 enemy(当前敌人对象)和 onAttack(攻击函数)作为 props

• 显示敌人的名称和战斗力,并提供一个按钮触发攻击。

GokuBattle 组件:

状态管理:

• currentEnemy:当前敌人对象,包含 namepower

• technique:悟空学会的招数对象,包含 namepowerBoost

powerLevel:

• 使用 useMemo 缓存战斗力的计算,只有当 currentEnemytechnique 变化时才重新计算。

• 计算公式:敌人的战斗力 + 招数的战斗力提升。

handleAttack:

• 使用 useCallback 缓存攻击函数,确保函数引用在组件重新渲染时保持不变。

• 当攻击一个敌人时,减少敌人的战斗力。

渲染:

• 显示悟空的总战斗力。

• 渲染当前敌人信息,通过 EnemyList 组件展示敌人信息并提供攻击按钮。

• 提供一个按钮让悟空学习新招数(例如“元气弹”),这会增加战斗力提升。

优化效果:

• useMemo:避免每次渲染都重新计算战斗力,只有当敌人或招数变化时才重新计算,提升性能。

• useCallback:确保攻击函数引用稳定,避免 EnemyList 组件因函数引用变化而重新渲染。

• React.memo:EnemyList 组件只有在 enemyonAttack 变化时才重新渲染,减少不必要的渲染次数。

这样一来,整个组件的性能得到了显著提升,避免了不必要的计算和渲染,仿佛悟空在战斗中释放了“终极元气弹”!

五、终极总结:掌握“超赛”优化的平衡

useMemo:适合于复杂计算场景,减少渲染中的重复计算。避免对简单运算使用,否则会增加内存负担。

正确使用: 复杂的战斗力计算、过滤、排序、大数据计算等。

错误使用: 简单的数值计算或依赖频繁变化的场景。

useCallback:用于缓存传递给子组件的回调函数,避免子组件重渲染。简单函数无需缓存,节省维护成本。

正确使用: 传递给依赖于回调的子组件的复杂函数,如攻击敌人的函数。

错误使用: 简单的日志函数或不影响渲染的函数。

React.memo:适用于渲染代价较高的子组件,避免重复渲染。

正确使用: 高渲染代价的敌人展示组件、复杂的展示组件如护罩显示。

错误使用: 小型、渲染代价低的按钮或标签等简单组件。

通过合理使用 useMemo、useCallback 和 React.memo,你可以大幅提升应用性能,轻松“超赛变身”。但这些优化工具也并非万能,不必要时不应滥用。真正的高级开发,不仅是了解这些工具,还要知道何时适度使用,掌握代码的“能量平衡”!


小闺女真的好可爱好可爱,软软糯糯的