[白话+有逻辑的解读]React中Hook函数详解——与性能优化相关的React.memo()、useMemo、useCallback和useEffect

发布于:2025-02-10 ⋅ 阅读:(67) ⋅ 点赞:(0)

理清一下这几个生命钩子

1.React.memo 【对组件层面】

此钩子主要避免在React中默认情况下存在的问题【父组件重新渲染子组件会默认重新渲染】,此hook通过判断props是否改变来判断子组件是否需要重新渲染,默认条件是浅比较props是否相等,用户也可实现自定义的比对函数(作为memo钩子的第二个参数)

// React.memo() 高阶组件,避免props不改变时的不必要的重新渲染(组件层面)
// 默认渲染:父组件渲染子组件一起渲染
// React.memo():props不变化时,子组件跳过渲染
// 默认情况是对props进行浅比较
// 可自定义一个比较函数作为第二个参数,用于判断何时应该重新渲染组件

import {memo} from "react";
function MyComponent(props){
}
// export default memo(MyComponent)

//比较函数
function arePropsEqual(prevProps,nextProps){
    return prevProps.name===nextProps.name;
}
export default memo(MyComponent,arePropsEqual)

2.useCallback 【对函数层面】

上述React.memo函数通过比对props是否改变来判断是否需要重新渲染子组件,而在React中props可以为函数,且在父组件重新渲染时,jsx会重新计算和评估,此过程jsx中定义的箭头函数会重构(引用会发生改变),从而导致子组件会有额外的开销,怎样避免此不必要的渲染,useCallback就派上用场!

// 高阶组件,避免props(函数)不改变时的不必要的重新渲染
//背景:如果每次父组件渲染都重新创建这个函数,即使函数的实现逻辑没有变化,子组件也可能会因为接收到的 prop 引用发生变化而重新渲染

//性能优化:当需要将回调函数作为 prop 传递给子组件,并且这些回调函数依赖于某些特定的值时
//可以使用 useCallback 来避免不必要的函数重新创建,从而提高性能。
//依赖项改变时重新创建回调函数
//避免子组件不必要的重新渲染,当prop为函数时
//利用memo+useCallback hooks 避免子组件重新渲染

//子组件
const MemoSon = memo(function Son(props){
        console.log("子组件渲染了")
        return <div>我是子组件</div>
    }
);

//!!!!!!在 React 组件的渲染过程中,如果在 JSX 中直接定义箭头函数,确实会在每次渲染时都产生一个新的函数实例
// 每次组件渲染时,JSX 都会被重新计算和评估,从而导致在其中定义的箭头函数被重新创建
function App(){
    const [count,setCount]=useState(0);

    //使用useCallback"记住这个函数"
    const newFn=useCallback(function fn(){},[]);
    console.log("父组件渲染");
    return(
        <div className="App">
            <MemoSon fn={newFn}></MemoSon>
            <button onClick={()=>setCount(count+1)}>点击渲染父组件</button>
        </div>
    )
}

3.useMemo() 【对数据层面】

useMemo 仅在依赖项发生变化时重新计算,当你想要避免在每次渲染时都重新执行昂贵的计算时,有点类似vue中的computed,但又有差异:useMemo是一个性能优化工具,用于避免不必要的昂贵计算。它返回一个记忆化的值,这个值在依赖项没有变化时保持不变,而computed一个声明式的计算属性,用于基于响应式依赖计算派生状态。它总是会在依赖变化时重新计算,并且具有缓存行为


//箭头函数返回对象字面量:const createPoint=()=>({x:0})
import {useEffect, useMemo, useState} from "react";

const LargeListComponent=()=>{
    const [list,setlist]=useState([]);
    const [filterText,setFilterText]=useState('');

    //模拟一个大型列表的加载
    useEffect(() => {
        //从后端API获取数据
        const largeList=Array.from({length:1000},(_,i)=>({
            id:i+1,
            name:`Item${i+1}`
        }));
        setlist(largeList)
    }, []);
    //使用useMemo来优化过滤操作
    const filteredList=useMemo(()=>{
        //只有列表list和过滤条件filterText变了才处理此计算
        return list.filter(item=>
            item.name.toLowerCase().includes(filterText.toLowerCase())
        );
    },[list,filterText])

    return(
        <div>
            <input
                type='text'
                value={filterText}
                onChange={(e)=>setFilterText(e.target.value)}
            />
            <ul>
                {filteredList.map(item=>(
                    <li key={item.id}>{item.name}</li>
                ))}
            </ul>
        </div>
    )
}
export default LargeListComponent;

4.useEffect【对副作用】

单词“Effect”有作用的意思,而在这特指副作用与UI渲染无关的操作,具体包括像后端获取数据、手动操作DOM、使用浏览器API和设置定时器或清理定时器等)。此钩子就是保证只有在特定依赖发生变化或者组件创建和销毁时才执行这些副作用

import {useEffect, useState} from "react";

function DataFetcher(){
    const [data,setData]=useState(null);
    const [query,setQuery]=useState('');
    useEffect(() => {
        fetch(`http://disaster.com/data?query=${query}`)
            .then(response=>response.json())
            .then(result=>setData(result))
            .catch(error=>console.error('Error fetching data:',error))
    }, [query]);

    return(
        <div>
            <input
                type='text'
                value={query}
                onChange={e=>setQuery(e.target.value)}
            />
            {data?(
                <div>
                    <h1>{data.title}</h1>
                </div>
            ):(
                <p>loading...</p>
            )
            }
        </div>
    )
}

钩子详解

React.memo()、useMemo、useCallback和useEffect是React框架中非常重要的性能优化和副作用处理钩子(Hook)。下面将分别对这四个钩子进行详细介绍:

1. React.memo()

  • 功能:React.memo()是一个高阶组件,用于对函数组件进行性能优化。它通过对组件的props进行浅比较,来决定组件是否需要重新渲染。
  • 使用场景:当父组件的数据变化时,如果子组件的props没有变化,但子组件仍然会跟随父组件重新渲染,这时可以使用React.memo()来避免不必要的重新渲染。
  • 示例
const ChildComp = (props) => {
  console.log('子组件渲染');
  return <div>我是子组件</div>;
};

const MemoChildComp = React.memo(ChildComp);

在父组件中,当传递给MemoChildComp的props没有变化时,子组件将不会重新渲染。

2. useMemo()

  • 功能:useMemo()是一个钩子,用于缓存计算结果,以避免在每次渲染时都重新进行计算。
  • 使用场景:当组件在渲染过程中需要进行昂贵的计算时,可以使用useMemo()来缓存计算结果,从而提高性能。
  • 语法
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

其中,computeExpensiveValue是需要进行缓存的昂贵计算函数,[a, b]是依赖项数组,只有当依赖项发生变化时,才会重新进行计算。

  • 注意事项:useMemo()应该只用于昂贵的计算,因为它本身也有一些开销。如果计算成本很低,或者输出不经常变化,那么在每次渲染时重新计算值可能会更好。

3. useCallback()

  • 功能:useCallback()是一个钩子,用于缓存回调函数,以避免在每次渲染时都创建新的函数引用。
  • 使用场景:当回调函数作为props传递给子组件时,如果父组件重新渲染导致回调函数引用变化,即使回调函数本身没有变化,子组件也可能因为props变化而重新渲染。这时可以使用useCallback()来避免不必要的重新渲染。
  • 语法
const memoizedCallback = useCallback(() => {
  doSomething();
}, [dependency]);

其中,doSomething是需要进行缓存的回调函数,[dependency]是依赖项数组,只有当依赖项发生变化时,才会创建新的函数引用。

4. useEffect()

  • 功能:useEffect()是一个钩子,用于在函数组件中执行副作用操作,如数据获取、订阅和手动更改DOM等。
  • 使用场景:当需要在组件渲染后执行某些操作,或者需要在组件卸载时清理资源时,可以使用useEffect()。
  • 语法
useEffect(() => {
  // 执行副作用操作
  return () => {
    // 清理操作(可选)
  };
}, [dependency]);

其中,[dependency]是依赖项数组,只有当依赖项发生变化时,才会执行副作用操作。如果传递一个空数组作为依赖项,则副作用函数只会在组件首次渲染时执行一次。

  • 注意事项:不要在useEffect()中执行任何返回值的操作,因为它的返回值不会被使用。如果需要执行清理操作,应该返回一个函数来执行清理逻辑。

综上所述,React.memo()、useMemo、useCallback和useEffect都是React中非常重要的钩子,它们各自有不同的功能和使用场景。合理使用这些钩子可以大大提高React应用的性能和用户体验。

JSX重新计算和评估过程

在 React 组件的渲染过程中,如果在 JSX 中直接定义箭头函数,确实会在每次渲染时都产生一个新的函数实例。

JSX(JavaScript XML)是 React 框架中使用的一种语法扩展,它允许在 JavaScript 代码中书写类似 HTML 的结构。在 React 组件的渲染过程中,JSX 会被重新计算和评估,这一过程涉及到多个步骤,以下是详细的解释:

1. JSX 的编译过程

首先,需要明确的是,JSX 并不是标准的 JavaScript 语法,因此浏览器无法直接识别和执行它。在开发过程中,通常使用 Babel 这样的编译器将 JSX 转换为标准的 JavaScript 代码。

  • Babel 编译:Babel 会将 JSX 语法转换为 React.createElement 函数调用。例如,JSX 代码 <div>Hello, world!</div> 会被转换为 React.createElement('div', null, 'Hello, world!')
  • /#PURE/ 标记:Babel 在编译 JSX 的同时,还会添加 /*#__PURE__*/ 标记。这个标记告诉打包工具(如 webpack)在生产模式下可以进行 tree shaking,从而移除未使用的代码,优化性能。

2. React 的渲染流程

当组件的状态(state)或属性(props)发生变化时,React 会触发重新渲染流程。

  • 虚拟 DOM 对比:React 使用虚拟 DOM 来表示 UI 的结构。当数据改变时,React 会创建一个新的虚拟 DOM 树,并与旧的虚拟 DOM 树进行对比。通过对比新旧虚拟 DOM 树的差异,React 能够找出需要更新的部分。
  • DOM 更新:根据虚拟 DOM 对比的结果,React 会将差异应用到实际的 DOM 上,从而更新 UI。

3. JSX 的重新计算和评估

在 React 组件的渲染过程中,JSX 会被重新计算和评估。这一过程涉及到以下几个步骤:

  • JSX 表达式计算:JSX 本质上是一个表达式,它会在组件的 render 方法(或函数组件的返回语句)中被计算和评估。当组件的状态或属性发生变化时,render 方法会被重新调用,JSX 表达式也会被重新计算和评估。
  • React.createElement 调用:编译后的 JSX 会被转换为 React.createElement 函数调用。在重新渲染过程中,这些函数调用会根据最新的状态和属性生成新的 React 元素。
  • 虚拟 DOM 树构建:这些新的 React 元素会被用来构建新的虚拟 DOM 树。

4. 性能优化

为了避免不必要的重新渲染和性能问题,可以采取以下优化措施:

  • PureComponent 或 React.memo:使用 PureComponent(针对类组件)或 React.memo(针对函数组件)来进行浅比较。这样,只有当 props 或 state 发生实际变化时,组件才会重新渲染。
  • shouldComponentUpdate:对于类组件,可以使用 shouldComponentUpdate 生命周期方法来控制组件是否应该重新渲染。
  • useCallback 和 useMemo:在函数组件中,可以使用 useCallback 来记忆化回调函数,避免因为函数引用变化而导致的组件重新渲染;使用 useMemo 来缓存计算结果,避免不必要的重新计算。

5. 注意事项

  • 避免在 JSX 中定义复杂逻辑:尽量将复杂逻辑放在 render 方法之外处理,以保持 JSX 的简洁和可读性。
  • 正确使用 key 属性:在列表渲染时,为遍历项添加唯一的 key 属性,以帮助 React 识别哪些项发生了改变、被添加或被删除,从而提高渲染性能。

综上所述,JSX 的重新计算和评估是 React 渲染流程中的重要环节。通过理解这一过程,并采取适当的优化措施,可以提高 React 应用的性能和用户体验。

useEffect hook中指的副作用包括?

在React中,useEffect钩子所提到的副作用(side effects)是指在组件渲染过程中,执行的一些不直接与渲染UI相关的操作。这些副作用操作可能会对组件的状态、DOM或外部资源产生影响。具体来说,副作用可以包括以下几个方面:

  1. 数据获取

    • 这是最常见的副作用之一。组件可能需要从服务器或其他数据源获取数据,以便在渲染时显示这些数据。例如,使用fetchaxios等API在组件挂载后获取数据,并将数据存储在组件的状态中。
  2. 订阅和取消订阅

    • 组件可能需要订阅某些外部数据源(如WebSocket连接、事件监听器等),以便在数据变化时更新组件的状态。当组件卸载时,需要取消这些订阅,以避免内存泄漏或其他问题。
  3. 手动操作DOM

    • 在某些情况下,组件可能需要直接操作DOM元素,例如设置焦点、滚动到特定位置或手动更改元素的样式。这些操作通常需要在组件渲染完成后进行。
  4. 使用浏览器API

    • 组件可能需要调用浏览器提供的API,如geolocation获取用户位置、localStorage存储数据等。这些操作也属于副作用范畴。
  5. 设置定时器或清理定时器

    • 组件可能需要设置定时器(如setIntervalsetTimeout)来执行周期性任务或延迟任务。当组件卸载时,需要清理这些定时器,以避免在组件不再需要时继续执行不必要的操作。
  6. 日志记录或调试

    • 在开发过程中,组件可能需要记录日志或进行调试操作。虽然这些操作通常不会在生产环境中执行,但它们仍然属于副作用范畴。

需要注意的是,副作用操作应该在useEffect钩子中进行,而不是在组件的渲染逻辑中直接执行。这是因为渲染逻辑应该专注于根据组件的状态和属性来生成UI,而副作用操作可能会引入不必要的复杂性或导致性能问题。通过将副作用操作封装在useEffect钩子中,React能够更好地管理这些操作的生命周期,并在必要时进行优化。

此外,在使用useEffect时,需要指定依赖项数组来确定何时触发副作用操作。如果依赖项数组为空,则副作用函数只会在组件首次渲染时执行一次。如果依赖项数组包含某些状态或属性,则每当这些依赖项发生变化时,都会重新执行副作用函数。这有助于确保副作用操作只在必要时执行,从而提高组件的性能和响应速度。


网站公告

今日签到

点亮在社区的每一天
去签到