React全家桶 -【高阶函数/高阶组件/钩子】-【forwardRef、mome、useImperativeHandle、useLayoutEffect】

高阶函数

forwardRef

  • 在 react 19 中,该API已经被废弃了;
  • 19版本中,可以将 ref 作为 props 进行传递;
tsx 复制代码
function MyInput({placeholder, ref}) {
 return <input placeholder={placeholder} ref={ref} />
}
//...

<MyInput ref={ref} />
  • forwardRef 是 React 中的一个高阶函数,用于将 ref 显式地传递给子组件。
    • 通常需要配合 useImperativeHandle 使用;
  • TypeScript 中使用 forwardRef 时,通常会配合泛型来指定 props 和 ref 的类型。

作用

  • 实际上函数式组件是没有ref的;
  • 那我们想要拿到函数式组件内部的某个DOM实例,就可以通过forwardRef将子组件的某个ref转发出去;

使用场景

  1. 父组件需要访问子组件的 DOM 节点或实例方法
    • 通过 forwardRef,父组件可以获取到子组件的 ref,并对其进行操作。
  2. 自定义组件包装原生组件
    • 当自定义组件内部使用了原生组件时,可以通过 forwardRef 将 ref 传递给原生组件。

示例代码

  • 假设你有一个 SubCom 组件,需要将其 ref 传递给父组件:
tsx 复制代码
import { useState, forwardRef, useRef, } from 'react';

const SubCom = forwardRef<HTMLInputElement, object>((props, ref) => {
  const [val, setVal] = useState('嘻哈少将');

  return (
    <div>
      <input ref={ref} type="text" value={val} onChange={(e) => setVal(e.target.value)} />
    </div>
  );
});

const ForwardRef: React.FC = () => {
  const subComRef = useRef(null);

  return (
    <>
      <SubCom ref={subComRef} />
      <button onClick={() => console.log(subComRef.current)}>LOG</button>
    </>
  );
};

export default ForwardRef;

注意事项

  • 类型安全
    • 在 TypeScript 中,使用 forwardRef 时需要明确指定 propsref 的类型,以确保类型安全;
    • forwardRef<RefType, PropsType> 有两个两个泛型参数:
      • 第一个是 ref 的类型;
      • 第二个是 props 的类型;
        • 若子组件不需要props,但是项目中配置了eslint,传递一个{}就会有提示(若没有eslint,就可以传递一个空对象),很难受,这时,我们可以将props的类型设置为object,就不会有eslint提示了;
  • 性能考虑
    • forwardRef 会增加组件的复杂度,因此只在必要时使用;

memo

  • memo 允许你在 组件props没有改变 的情况下 跳过重新渲染
  • memo 是 React 中的一个高阶组件,用于优化函数组件的性能。
  • 它通过记忆化(Memoization)技术,避免在 props 或 state 没有变化时不必要的重新渲染。这对于性能敏感的应用非常有用。

使用场景

  1. 性能优化
    • 当组件的渲染开销较大,且其 props 很少发生变化时,使用 memo 可以显著提升应用性能。
  2. 避免不必要的渲染 :、
    • 确保只有在 props 发生变化时才进行重新渲染。

示例代码

  • 假设你有一个 Summary 组件,希望使用 memo 来优化其性能
tsx 复制代码
import React, { memo } from 'react';
interface SummaryProps {
    title: string;
    data: any[];
}
const Summary = (props: SummaryProps) => {
    console.log('SummaryComponent rendered');
    return (
        <div>
            <h1>{props.title}</h1>
            {/* 渲染数据 */}
            <ul>
                {props.data.map((item, index) => ( <li key={index}>{item}</li> ))}
            </ul>
        </div>
    );
};

export default memo(Summary); 

解释

  • memo :
    • 用于包裹 Summary,使其成为一个记忆化的组件。
  • Summary :
    • 这是你实际的函数组件,负责渲染内容。
  • console.log :
    • 用于调试,观察组件是否在每次父组件更新时都重新渲染。

自定义比较逻辑

  • 默认情况下,memo 使用浅比较来判断 props 是否发生变化。如果需要更复杂的比较逻辑,可以传递一个自定义的比较函数作为第二个参数:
tsx 复制代码
const areEqual = (prevProps: SummaryProps, nextProps: SummaryProps) => {
    return prevProps.title === nextProps.title && prevProps.data === nextProps.data;
};
const Summary = memo(SummaryComponent, areEqual); 

注意事项

  • 浅比较
    • 默认情况下,memo 使用浅比较来判断 props 是否发生变化。
    • 如果 props 是对象或数组,浅比较可能不够精确。
  • 性能影响
    • 虽然 memo 可以提高性能,但如果过度使用或不正确使用,可能会导致性能下降。
    • 因此,只在必要时使用 memo

‼️ 注意

  • 使用 React.memo 缓存组件时,组件的 内部状态 不会被 重置 (即通过 useStateuseReducer 创建的状态)。
  • React.memo 只是用于优化组件的渲染性能,它会比较组件的 props,如果 props 没有变化,则不会重新渲染组件,而是复用之前的渲染结果。
  • 然而,需要注意的是,React.memo 只是比较 props 的浅层引用相等性。如果 props 中的对象或数组发生变化,即使内容相同,也会被视为不同的 props,从而导致组件重新渲染。

组件销毁

  • 使用 React.memo 缓存的组件不会因为缓存机制而被销毁和重新创建。
  • React.memo 主要用于优化组件的渲染性能,通过比较组件的 props 来决定是否需要重新渲染。
  • 如果 props 没有变化,组件将不会重新渲染,而是复用之前的渲染结果。

关键点

  1. 组件的生命周期
    • React.memo 不会影响组件的生命周期。组件的挂载(mounting)和卸载(unmounting)仍然遵循 React 的标准生命周期规则。
    • 如果组件的父组件重新渲染,并且传递给 React.memo 组件的 props 没有变化,那么 React.memo 组件不会重新渲染,但也不会被销毁和重新创建。
  2. 销毁和重新创建
    • 组件的销毁和重新创建通常发生在以下情况:
      • 父组件的逻辑导致组件被移除或添加到 DOM 中。
      • 组件的 key 属性发生变化,React 会认为这是一个新的组件实例,从而销毁旧的实例并创建新的实例。

useImperativeHandle

  • useImperativeHandle 是 React 中的一个 Hook,用于自定义暴露给父组件的 ref。
  • 通过 useImperativeHandle,你可以控制哪些方法或属性可以通过 ref 访问,从而实现更细粒度的控制。

使用场景

  1. 自定义 ref 行为
    • 允许你在子组件中自定义通过 ref 暴露的方法或属性(自定义暴露给父组件的属性或方法
  2. 父组件调用子组件的方法
    • 父组件可以通过 ref 调用子组件中的方法,而不仅仅是访问 DOM 节点。

示例代码

  • 假设你有一个子组件 SubCom,希望父组件能够通过 ref 调用子组件中的某个方法 或 访问某个属性:

    tsx 复制代码
    import { useState, forwardRef, useRef, useImperativeHandle, } from 'react';
    
    interface SubComType {
      val: string;
      onTest: () => void;
    };
    
    const SubCom = forwardRef<SubComType, object>((props, ref) => {
      const [val, setVal] = useState('嘻哈少将');
      const onTest = () => {
        console.log('onTest');
      };
    
      // 自定义需要暴露给父组件的属性或方法
      useImperativeHandle(ref, () => ({
        val,
        onTest,
      }));
    
      return (
        <div>
          <input type="text" value={val} onChange={(e) => setVal(e.target.value)} />
        </div>
      );
    });
    
    const ForwardRef: React.FC = () => {
      const subComRef = useRef<SubComType>(null);
    
      return (
        <>
          <div>ForwardRef</div>
          <SubCom ref={subComRef} />
          <button onClick={() => console.log(subComRef.current)}>LOG</button>
          <button onClick={() => subComRef.current?.onTest()}>调用子组件方法 - onTest</button>
        </>
      );
    };
    
    export default ForwardRef;

解释

  • forwardRef :
    • 用于将 ref 传递给子组件。
  • useImperativeHandle :
    • 用于自定义通过 ref 暴露的方法或属性。
  • SubComType :
    • 定义了通过 ref 暴露的方法或属性的类型。
  • subComRef :
    • 用于引用自组件的 ref;
    • onTest 方法 : 通过 useImperativeHandle 暴露给父组件的方法。

注意事项

  • 类型安全
    • 在 TypeScript 中,使用 useImperativeHandle 时需要明确指定 ref 的类型,以确保类型安全。
  • 谨慎使用
    • 过度使用 useImperativeHandle 可能会导致组件之间的耦合度增加,影响代码的可维护性。 希望这些信息对你有帮助!如果有更多问题,请随时提问。

useLayoutEffect

  • useLayoutEffect 是 React 中的一个 Hook,类似于 useEffect,但它在 所有的 DOM 变更之后 同步 调用,但在 浏览器绘制之前
  • 这意味着 useLayoutEffect 可以读取和修改 DOM 布局,而不会导致视觉上的闪烁或抖动。

主要用途

  1. 读取布局信息
    • 在组件渲染后立即读取 DOM 节点的布局信息,如宽度、高度等。
  2. 同步更新 DOM
    • 在浏览器绘制之前同步更新 DOM,确保用户看到的是最新的布局。

useEffect 的区别

  • 执行时机
    • useEffect
      • 在浏览器绘制之后异步执行。
    • useLayoutEffect
      • 在浏览器绘制之前同步执行。
  • 适用场景
    • useEffect
      • 适用于大多数副作用操作,如数据获取、订阅事件等。
    • useLayoutEffect
      • 适用于需要在浏览器绘制之前读取和修改 DOM 布局的场景。

示例代码

  • Summary 组件中,useLayoutEffect 用于计算进度条的宽度,并根据数据动态调整进度条的样式。
tsx 复制代码
useLayoutEffect(() => {
    if (info[dateType].length === 0) return;
    const boxWidth = 258;
    const max = Math.max(...info[dateType]);
    const maxIndex = info[dateType].findIndex(i => i === max);
    const moneyWidth = document.querySelectorAll('.SummaryBoxItemMoney')
        ?.[maxIndex]
        ?.getBoundingClientRect()
        ?.width || 0;
    const progressWidth = boxWidth - moneyWidth;
    const perWidth = progressWidth / max;
    setProgressWidth(info[dateType].map(i => i * perWidth));
}, [info, dateType]); 

解释

  • 条件检查
    • if (info[dateType].length === 0) 确保在没有数据时不会进行后续计算。
  • 常量定义
    • boxWidth: 进度条容器的宽度。
    • max: 当前日期类型下的最大值。
    • maxIndex: 最大值的索引。
  • 计算 moneyWidth
    • 通过 getBoundingClientRect 获取最大值对应的 DOM 元素的宽度。
  • 计算 progressWidth
    • 进度条容器的总宽度减去最大值对应的 DOM 元素的宽度。
  • 计算 perWidth
    • 每个单位值对应的进度条宽度。
  • 更新 progressWidth
    • 根据每个值计算出对应的进度条宽度,并更新状态。

注意事项

  • 性能影响
    • 由于 useLayoutEffect 是同步执行的,频繁使用可能会导致性能问题。
    • 因此,只在确实需要在浏览器绘制之前读取和修改 DOM 布局时使用。
  • 副作用
    • useLayoutEffect 中的副作用会阻塞浏览器的绘制,因此应尽量减少其内部的复杂计算和 DOM 操作。
相关推荐
@大迁世界5 小时前
彻底改变我 React 开发方式的组件模式
前端·javascript·react.js·前端框架·ecmascript
DCTANT9 小时前
【原创】vue-element-admin-plus完成确认密码功能,并实时获取Form中表单字段中的值
前端·javascript·vue.js·elementui·typescript
黑土豆10 小时前
TypeScript技术系列13:深入理解配置文件tsconfig.json
前端·javascript·typescript
少卿11 小时前
深入理解 useContext:从原理到实现
前端·react.js
羊聪11 小时前
SSE数据流的基本实现与处理:示例与解析
前端·typescript
@PHARAOH11 小时前
WHAT - React 两个重要的 Typescript 类型:ReactNode vs JSX.Element
javascript·react.js·typescript
祈澈菇凉11 小时前
什么是 React Router?如何使用?
javascript·react.js·ecmascript
林同学++11 小时前
react中通过 EventEmitter 在组件间传递状态
前端·react.js·前端框架
我自纵横202313 小时前
Vue 3 中 ref 与 reactive 的对比
前端·javascript·vue.js·typescript·前端框架·html5
lvbb6613 小时前
react动态路由
前端·react.js·前端框架