React Hooks中memo、useMemo、useCallBack的作用

一句话概括

memouseMemouseCallBack主要用于避免 React 组件的重复渲染,作为 性能优化 的一种手段,你可以根据场景合理的使用它们。

React组件的更新机制

在使用memo、useCallBack、useMemo前,我们需要先了解React组件的更新机制:React组件在默认情况下,父组件或兄弟组件触发更新后,会按照父组件、子组件的顺序重新渲染,并且即使子组件本身没有发生任何变化,也会重复触发更新。

举一个简单的例子🌰, 目前我们有Parent、Child1、Child2 三个组件。

// parent.jsx
import Child1 from './child1';
import Child2 from './child2';
import { useState } from 'react';

const Parent = () => {
  const [count, setCount] = useState(0);
  console.log('Parent 组件更新');
  return (
    <div>
      <div>计数: {count}  <button onClick={() => setCount(count + 1)}>自增</button></div>
      <Child1 />
      <Child2/>
    </div>
  );
};
export default Parent;
// child1.jsx
const Child1 = () => {
  console.log('Child1 组件更新');
  return (
    <div>
      我是Child1
    </div>
  );
};
export default Child1;
// child2.jsx
const Child2 = () => {
  console.log('Child2 组件更新');
  return (
    <div>
      我是Child2
    </div>
  );
};
export default Child2;

我们观察控制台,发现三个组件的 function 全部被重新执行了,即使在这次更新中 Child1、Child2 组件的内容完全没有发生变化!

如果在实际项目中, Child1、Child2 组件包含高开销的计算,Parent 组件的更新会导致它们不断地重复渲染,这样会对性能产生比较大的影响。

那么有没有什么办法可以避免这种情况下的重复渲染,从而达到性能优化的目的?这个就是我们使用memo、useCallBack、useMemo的原因。

memo

如果你的组件不存在 props 或者 props 相同的情况下,使用 React.memo包裹(高阶组件的形式),可以避免组件多余无意义的更新动作。
React.memo 通过记忆组件渲染结果的方式来提高组件的性能表现,既React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

// parent.jsx
import Child1 from './child1';
import Child2 from './child2';
import { useState } from 'react';

const Parent = () => {
  const [count, setCount] = useState(0);
  console.log('Parent 组件更新');
  const params = {msg:'哈哈'};
  return (
    <div>
      <div>计数: {count}  <button onClick={() => setCount(count + 1)}>自增</button></div>
      <Child1 />
      <Child2 params={params}/>
    </div>
  );
};
export default Parent;
// child1.jsx
import { memo } from "react";

const Child1 = memo(() => {
  console.log('Child1 组件更新');
  return (
    <div>
      我是Child1
    </div>
  );
});
export default Child1;
// child2.jsx
import { memo } from "react";

const Child2 = memo((props: any) => {
  console.log('Child2 组件更新');
  return (
    <div>
      我是Child2
    </div>
  );
});
export default Child2;

由于我们点击了按钮,触发了state的更新,Parent 组件的 function 被重新执行。但是因为 Child1 组件因为包裹了 memo,所以此次更新 并未牵连 子组件 Child1 一同更新 既执行 它的 function。那为什么同样包裹的 Child2 会重新执行呢?

这是因为 memo 在判断props是否变化时,是进行的浅比较,比如 空值 或 基本类型 的 props,但是若传递的属性是是引用类型的属性, 则在父组件在更新的时候 属性 params 都会被重新定义一遍,进而导致包裹 Child2 的 memo 内部认为 props 发生了变化,最终重新渲染了 Child2 组件。

useMemo正用于解决这样的问题!

useMemo

useMemo接受两个参数 创建函数依赖项数组,它仅会在依赖项数组中的元素 发生改变时,才重新计算 memoized 值(通过 创建函数 return 出去)。这种优化有助于避免在每次渲染时都进行高开销的计算。

因此上面的代码我们完全可以这样改写:

// parent.jsx
import Child from './child';
import { useMemo, useState } from 'react';

const Parent = () => {
  const [count, setCount] = useState(0);
  console.log('Parent 组件更新');
  const params = useMemo(()=>({msg:'哈哈'}),[])
  return (
    <div>
      <div>计数: {count}  <button onClick={() => setCount(count + 1)}>自增</button></div>
      <Child params={params}/>
    </div>
  );
};
export default Parent;
// child.jsx
import { memo } from "react";

const Child = memo((props: any) => {
  console.log('Child 组件更新');
  return (
    <div>
      我是Child
    </div>
  );
});
export default Child;

可以看到 Child并无重新执行和渲染,这是由于Parent 中的 params 是一个 useMemo ,其方法内部本身没有依赖任何变量,因此它的依赖数组项为空。

这样做可以保证无论 Parent 组件是否更新,params 变量始终都会是同一个。进而也就不会出发 结合了memo的 Child 组件更新了。

至此,我们成功通过使用memo、useMemo的组合达到了我们最终的目标。

useCallBack

本质是 useMemo 的语法糖!用法唯一的区别是:useMemo返回的是传入的回调函数的执行结果,useCallBack返回的是传入的回调函数。

useCallBack 的使用场景是 当传递给子组件的属性是一个函数的时候, 返回该函数的引用。当依赖项变化时,返回新函数的引用;否则返回缓存的旧函数引用:简单来说就是 useMemo 适合 缓存 非函数的属性,而 useCallBack 适合 缓存 函数的属性。

// parent.jsx
import Child from './child';
import { useCallback, useState } from 'react';

const Parent = () => {
  const [count, setCount] = useState(0);
  console.log('Parent 组件更新');
  const sayHello = useCallback(() => {
    console.log('你好');
  }, []);

  return (
    <div>
      <div>
        计数: {count} <button onClick={() => setCount(count + 1)}>自增</button>
      </div>
      <Child params={sayHello} />
    </div>
  );
};
export default Parent;
// child.jsx
import { memo } from "react";

const Child = memo((props: any) => {
  console.log('Child 组件更新');
  return (
    <div>
      我是Child, <button onClick={()=>props.params()}>Child按钮</button>
    </div>
  );
});
export default Child;

其它

对比 vue computed

从设计初衷来看,Vue 的 computedReact 的 useMemo 有着相似的本意,即通过缓存计算结果来优化性能,避免不必要的重复计算。它们都旨在解决以下问题:

  1. 避免重复计算:当某些值依赖于其他状态或数据时,如果每次渲染都重新计算,可能会浪费性能。
  2. 响应式更新:当依赖项变化时,自动重新计算并更新结果。
  3. 简化代码逻辑:将复杂的计算逻辑封装起来,使代码更清晰、更易维护。

目标 Vue 的 computed React 的 useMemo
缓存计算结果 通过缓存计算结果,避免重复计算。 通过缓存计算结果,避免重复计算。
响应式更新 当依赖的响应式数据变化时,自动重新计算。 当依赖项变化时,重新计算并返回新的值。
简化代码逻辑 将复杂的计算逻辑封装到 computed 中。 将复杂的计算逻辑封装到 useMemo 中。

与useEffect对比

useMemo 是同步执行,而 useEffect 是异步的。

import { useMemo, useState, useEffect } from 'react';

function TestComponent() {
  const [count, setCount] = useState(0);

  console.log('普通代码:在渲染期间同步执行');

  useMemo(() => {
    console.log('useMemo:在渲染期间同步执行');
  }, [count]);

  useEffect(() => {
    console.log('useEffect:在渲染后异步执行,不阻塞渲染)');
  }, [count]);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
export default TestComponent;

对渲染流程的影响

  • useMemo

    • 同步执行,会阻塞渲染流程。如果 useMemo 的回调函数中有复杂的计算,可能导致渲染延迟。
    • 适用于轻量级计算或必须立即使用的值(如派生状态)。
    • 如果计算结果需要参与当前渲染,必须使用 useMemo
  • useEffect

    • 异步执行,不会阻塞渲染流程,适合处理副作用(如网络请求、DOM 操作)。
    • 副作用操作不会影响当前渲染的结果,但可能会触发后续的重新渲染。

useMemo 的适用场景

  1. 派生状态计算
    根据 propsstate 计算出一个新的值,且需要立即在渲染中使用。

    const total = useMemo(() => items.reduce((sum, item) => sum + item.price, 0), [items]);
    
  2. 避免引用类型重新创建
    缓存对象或数组,避免子组件因引用变化而重新渲染。

    const config = useMemo(() => ({ timeout: 1000 }), []); // 依赖为空,引用不变
    
  3. 性能敏感的计算
    当计算成本较高时(如大数据过滤、排序)。

    const filteredList = useMemo(() => {
      return largeList.filter(item => item.isActive);
    }, [largeList]); // 仅当 largeList 变化时重新计算
    

useEffect 的适用场景

  1. 副作用操作
    如网络请求、DOM 操作、订阅事件等。

    useEffect(() => {
      fetchData().then(data => setData(data));
    }, []);
    
  2. 响应状态变化
    当某些状态变化后需要执行特定操作(如数据保存)。

    useEffect(() => {
      saveToLocalStorage(user);
    }, [user]); // user 变化时触发保存
    
  3. 清理操作
    在组件卸载或依赖变化前执行清理(如取消订阅)。

    useEffect(() => {
      const subscription = eventEmitter.subscribe(handleEvent);
      return () => subscription.unsubscribe(); // 清理函数
    }, []);
    

不要滥用

使用useMemo、useCallBack时,本身会产生额外的开销,并且这两个方法必须和memo搭配使用,否则很可能会变成负优化。

因此,在实际项目中,需要结合实际场景,评估重复渲染和创建useCallBack/useCallBack的开销来判断到底用不用useCallBack、useMemo。

posted @   丁少华  阅读(18)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示