MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

useRef Hook在React中的多种用途

2021-01-256.5k 阅读

一、useRef Hook 的基本概念

在 React 开发中,useRef 是一个非常有用的 Hook。它返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。这个 ref 对象在组件的整个生命周期内保持不变。

从本质上来说,useRef 创建的 ref 就像是一个“盒子”,可以在里面存放任何值,并且这个“盒子”不会因为组件的重新渲染而被重新创建。这与在函数组件中声明的普通变量不同,普通变量在每次函数组件重新渲染时都会重新声明和初始化。

import React, { useRef } from 'react';

const RefExample = () => {
  const ref = useRef(0);
  return (
    <div>
      <p>{ref.current}</p>
    </div>
  );
};

export default RefExample;

在上述代码中,我们使用 useRef 创建了一个 ref 对象,并将其初始值设为 0。通过 ref.current 我们可以访问和修改这个值。

二、在 DOM 操作中的应用

  1. 获取 DOM 元素引用 在 React 中,直接操作 DOM 并不是常见的做法,但在某些场景下,比如聚焦输入框、滚动到某个元素等,我们需要获取 DOM 元素的引用。useRef 提供了一种简单的方式来实现这一点。
import React, { useRef, useEffect } from'react';

const InputFocus = () => {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <input type="text" ref={inputRef} />
    </div>
  );
};

export default InputFocus;

在这个例子中,我们创建了一个 inputRef,并将其传递给 <input> 元素的 ref 属性。通过 useEffect Hook,在组件挂载后,我们调用 inputRef.current.focus() 来自动聚焦输入框。这里的 null 是初始值,因为在组件挂载前,DOM 元素还不存在。

  1. 操作 DOM 元素属性和方法 除了聚焦,我们还可以利用 useRef 来操作 DOM 元素的其他属性和方法。例如,我们可以获取一个视频元素的引用,然后控制其播放和暂停。
import React, { useRef } from'react';

const VideoControl = () => {
  const videoRef = useRef(null);

  const playVideo = () => {
    if (videoRef.current) {
      videoRef.current.play();
    }
  };

  const pauseVideo = () => {
    if (videoRef.current) {
      videoRef.current.pause();
    }
  };

  return (
    <div>
      <video ref={videoRef} src="your-video-url.mp4" controls />
      <button onClick={playVideo}>Play</button>
      <button onClick={pauseVideo}>Pause</button>
    </div>
  );
};

export default VideoControl;

在上述代码中,我们通过 videoRef 获取视频元素的引用,并在按钮的点击事件中调用 playpause 方法来控制视频的播放和暂停。

三、保存可变值,不触发重新渲染

  1. 跨渲染保存状态 在 React 中,状态(state)的变化会导致组件重新渲染。但有些时候,我们需要保存一些可变的值,并且不希望因为这些值的变化而触发组件重新渲染。useRef 就可以满足这个需求。
import React, { useRef } from'react';

const Counter = () => {
  const countRef = useRef(0);
  const increment = () => {
    countRef.current++;
    console.log(countRef.current);
  };

  return (
    <div>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;

在这个计数器的例子中,我们使用 useRef 来保存计数器的值。每次点击按钮,countRef.current 的值会增加,但由于 useRef 的值变化不会触发组件重新渲染,所以页面上不会有视觉上的更新。如果我们使用 useState 来实现同样的功能,每次状态变化都会导致组件重新渲染。

  1. 保存函数引用 有时候我们需要在组件中保存一个函数的引用,并且希望这个引用在组件的整个生命周期内保持不变。useRef 可以用来实现这一点。
import React, { useRef } from'react';

const FunctionRefExample = () => {
  const myFunctionRef = useRef(() => {
    console.log('This is a function saved in ref');
  });

  const callFunction = () => {
    myFunctionRef.current();
  };

  return (
    <div>
      <button onClick={callFunction}>Call Function</button>
    </div>
  );
};

export default FunctionRefExample;

在上述代码中,我们使用 useRef 保存了一个函数引用。通过 myFunctionRef.current 可以随时调用这个函数。由于 useRef 保存的函数引用不会因为组件重新渲染而改变,所以在一些依赖于函数引用稳定性的场景中非常有用,比如在 setInterval 或者 addEventListener 中使用函数引用时。

四、在自定义 Hook 中的应用

  1. 创建带有持久化数据的自定义 Hook 我们可以利用 useRef 在自定义 Hook 中创建持久化的数据。例如,我们创建一个自定义 Hook 来记录函数被调用的次数。
import { useRef } from'react';

const useCallCount = () => {
  const callCountRef = useRef(0);
  const incrementCallCount = () => {
    callCountRef.current++;
  };
  return {
    callCount: callCountRef.current,
    incrementCallCount
  };
};

export default useCallCount;

然后在组件中使用这个自定义 Hook:

import React from'react';
import useCallCount from './useCallCount';

const CallCounterComponent = () => {
  const { callCount, incrementCallCount } = useCallCount();

  return (
    <div>
      <p>Function has been called {callCount} times.</p>
      <button onClick={incrementCallCount}>Increment Call Count</button>
    </div>
  );
};

export default CallCounterComponent;

在这个例子中,useCallCount 自定义 Hook 使用 useRef 来保存函数被调用的次数。每次调用 incrementCallCount 函数,callCountRef.current 的值会增加,并且这个值在组件的多次渲染中保持不变。

  1. 利用 useRef 实现自定义 Hook 的缓存机制 假设我们有一个自定义 Hook,用于获取某个数据,并且希望在数据没有变化时,避免重复获取。我们可以使用 useRef 来实现缓存机制。
import { useRef } from'react';

const useCachedData = (fetchFunction) => {
  const cachedDataRef = useRef(null);
  const data = cachedDataRef.current;
  if (!data) {
    const newData = fetchFunction();
    cachedDataRef.current = newData;
    return newData;
  }
  return data;
};

export default useCachedData;

在组件中使用这个自定义 Hook:

import React from'react';
import useCachedData from './useCachedData';

const MyComponent = () => {
  const fetchData = () => {
    // 模拟异步数据获取
    return { message: 'Cached Data' };
  };
  const data = useCachedData(fetchData);

  return (
    <div>
      <p>{data.message}</p>
    </div>
  );
};

export default MyComponent;

在这个例子中,useCachedData 自定义 Hook 使用 useRef 来缓存数据。第一次调用时,它会执行 fetchFunction 获取数据并缓存起来。后续调用时,如果缓存中有数据,就直接返回缓存的数据,从而避免了重复的数据获取操作。

五、在类组件与函数组件交互中的用途

  1. 从函数组件访问类组件的实例方法 在 React 应用中,可能会存在函数组件和类组件混合使用的情况。有时候,函数组件需要访问类组件的实例方法。我们可以使用 useRef 来实现这一点。

首先,定义一个类组件:

import React, { Component } from'react';

class MyClassComponent extends Component {
  myMethod = () => {
    console.log('This is a method in MyClassComponent');
  };
  render() {
    return <div>Class Component</div>;
  }
}

export default MyClassComponent;

然后,在函数组件中使用 useRef 来访问类组件的实例方法:

import React, { useRef } from'react';
import MyClassComponent from './MyClassComponent';

const FunctionComponent = () => {
  const classComponentRef = useRef(null);

  const callClassMethod = () => {
    if (classComponentRef.current) {
      classComponentRef.current.myMethod();
    }
  };

  return (
    <div>
      <MyClassComponent ref={classComponentRef} />
      <button onClick={callClassMethod}>Call Class Method</button>
    </div>
  );
};

export default FunctionComponent;

在上述代码中,我们在函数组件 FunctionComponent 中创建了一个 classComponentRef,并将其传递给 MyClassComponentref 属性。通过 classComponentRef.current,我们可以在函数组件中访问类组件的 myMethod 方法。

  1. 从类组件访问函数组件的内部状态或方法(通过 useRef 间接实现) 虽然在 React 中,从类组件直接访问函数组件的内部状态或方法不是常见的做法,但在某些复杂场景下可能有需求。我们可以通过在函数组件中使用 useRef 并将 ref 对象传递给类组件,从而让类组件间接访问函数组件的相关信息。

首先,定义一个函数组件:

import React, { useRef } from'react';

const FunctionComp = () => {
  const internalValueRef = useRef(0);
  const incrementValue = () => {
    internalValueRef.current++;
  };
  return (
    <div>
      <p>Internal Value: {internalValueRef.current}</p>
    </div>
  );
};

export default FunctionComp;

然后,定义一个类组件来访问函数组件的 incrementValue 方法:

import React, { Component } from'react';
import FunctionComp from './FunctionComp';

class ClassComp extends Component {
  constructor(props) {
    super(props);
    this.functionCompRef = React.createRef();
  }

  callFunctionCompMethod = () => {
    if (this.functionCompRef.current) {
      this.functionCompRef.current.incrementValue();
    }
  };

  render() {
    return (
      <div>
        <FunctionComp ref={this.functionCompRef} />
        <button onClick={this.callFunctionCompMethod}>Increment Function Comp Value</button>
      </div>
    );
  }
}

export default ClassComp;

在这个例子中,函数组件 FunctionComp 使用 useRef 保存内部值并提供一个 incrementValue 方法。类组件 ClassComp 通过 React.createRef 创建一个 ref,并将其传递给 FunctionComp。通过这个 ref,类组件可以调用函数组件的 incrementValue 方法,实现了类组件对函数组件内部方法的间接访问。

六、useRef 与 useMemo、useCallback 的区别

  1. useRef 与 useMemo
    • 本质区别useMemo 用于记忆化计算结果,它会在依赖项发生变化时重新计算并返回新的值,主要用于性能优化,避免不必要的计算。而 useRef 是用于创建一个可变的 ref 对象,其值可以随时更改,并且不会因为值的变化而触发组件重新渲染。
    • 使用场景区别:当你有一个复杂的计算,并且希望在依赖项不变时避免重复计算,就使用 useMemo。例如,计算一个数组中所有元素的总和,并且数组在组件渲染过程中不经常变化,就可以使用 useMemo 来缓存计算结果。而当你需要保存一个可变的值,并且这个值的变化不应该导致组件重新渲染,比如保存定时器的 ID 或者在 DOM 操作中获取元素引用,就使用 useRef
    • 代码示例
import React, { useMemo, useRef } from'react';

const MemoAndRefExample = () => {
  const numRef = useRef(0);
  const incrementRef = () => {
    numRef.current++;
  };

  const expensiveCalculation = (a, b) => {
    // 模拟复杂计算
    for (let i = 0; i < 1000000; i++) {
      // 一些计算操作
    }
    return a + b;
  };

  const result = useMemo(() => expensiveCalculation(10, 20), []);

  return (
    <div>
      <p>Ref value: {numRef.current}</p>
      <button onClick={incrementRef}>Increment Ref</button>
      <p>Memo result: {result}</p>
    </div>
  );
};

export default MemoAndRefExample;

在上述代码中,numRef 使用 useRef 来保存一个可变值,点击按钮改变 numRef.current 不会触发组件重新渲染。而 result 使用 useMemo 来缓存 expensiveCalculation 的计算结果,由于依赖项数组为空,这个计算只会在组件挂载时执行一次。

  1. useRef 与 useCallback
    • 本质区别useCallback 用于记忆化函数,返回一个记忆化后的函数,只有当依赖项发生变化时,才会返回新的函数。它主要用于防止函数在组件重新渲染时不必要的重新创建,从而避免一些性能问题,特别是在将函数作为 prop 传递给子组件时。而 useRef 保存的函数引用不会因为组件重新渲染而改变,它更侧重于在组件内部保存一个函数引用,并且这个引用在整个组件生命周期内稳定不变。
    • 使用场景区别:当你要将一个函数作为 prop 传递给子组件,并且希望在父组件重新渲染时,如果依赖项没有变化,子组件不会因为接收到新的函数引用而重新渲染,就使用 useCallback。例如,在一个列表组件中,父组件传递一个点击处理函数给子列表项组件,使用 useCallback 可以确保只有当点击处理函数的依赖项变化时,子列表项组件才会因为接收到新的函数引用而重新渲染。而当你在组件内部需要保存一个函数引用,用于在不同的生命周期阶段或者事件处理中调用,并且希望这个引用稳定不变,就使用 useRef
    • 代码示例
import React, { useCallback, useRef } from'react';

const CallbackAndRefExample = () => {
  const myFunctionRef = useRef(() => {
    console.log('Function in ref');
  });

  const myCallbackFunction = useCallback(() => {
    console.log('Callback function');
  }, []);

  const callRefFunction = () => {
    myFunctionRef.current();
  };

  return (
    <div>
      <button onClick={callRefFunction}>Call Ref Function</button>
      {/* 这里可以将 myCallbackFunction 作为 prop 传递给子组件 */}
    </div>
  );
};

export default CallbackAndRefExample;

在上述代码中,myFunctionRef 使用 useRef 保存一个函数引用,callRefFunction 可以随时调用这个函数。myCallbackFunction 使用 useCallback 记忆化一个函数,由于依赖项为空,这个函数在组件的整个生命周期内引用不变,适合作为 prop 传递给子组件以避免子组件不必要的重新渲染。

七、useRef 在性能优化中的注意事项

  1. 避免不必要的 DOM 操作 虽然 useRef 方便我们进行 DOM 操作,但过度或不必要的 DOM 操作会影响性能。例如,在一个频繁渲染的组件中,如果每次渲染都通过 useRef 去操作 DOM,可能会导致性能问题。我们应该尽量减少 DOM 操作的频率,只在必要的时候进行操作。
import React, { useRef, useEffect } from'react';

const UnoptimizedDOMUpdate = () => {
  const divRef = useRef(null);
  useEffect(() => {
    if (divRef.current) {
      divRef.current.style.color = 'red';
    }
  });

  return (
    <div ref={divRef}>This is a div</div>
  );
};

export default UnoptimizedDOMUpdate;

在上述代码中,如果这个组件频繁重新渲染,每次渲染都设置 div 的颜色,这是不必要的。我们可以通过在依赖项数组中添加必要的条件来优化,比如只有当某个状态变化时才进行 DOM 操作。

import React, { useRef, useEffect, useState } from'react';

const OptimizedDOMUpdate = () => {
  const divRef = useRef(null);
  const [shouldUpdateColor, setShouldUpdateColor] = useState(false);

  useEffect(() => {
    if (shouldUpdateColor && divRef.current) {
      divRef.current.style.color = 'red';
    }
  }, [shouldUpdateColor]);

  const toggleColor = () => {
    setShouldUpdateColor(!shouldUpdateColor);
  };

  return (
    <div>
      <div ref={divRef}>This is a div</div>
      <button onClick={toggleColor}>Toggle Color</button>
    </div>
  );
};

export default OptimizedDOMUpdate;

在优化后的代码中,只有当 shouldUpdateColor 状态变化时,才会通过 useRef 操作 DOM 元素的颜色,减少了不必要的 DOM 操作。

  1. 合理使用 useRef 保存数据 当使用 useRef 保存数据时,要注意数据的变化是否会影响组件的逻辑。如果使用不当,可能会导致数据不一致或逻辑错误。例如,在一个需要根据某个值进行条件渲染的组件中,如果错误地使用 useRef 保存这个值,可能会因为 useRef 的值变化不触发重新渲染,导致条件渲染不正确。
import React, { useRef } from'react';

// 错误示例
const IncorrectUseRef = () => {
  const showTextRef = useRef(false);
  const toggleText = () => {
    showTextRef.current =!showTextRef.current;
  };

  return (
    <div>
      <button onClick={toggleText}>Toggle Text</button>
      {showTextRef.current && <p>Text should show or hide</p>}
    </div>
  );
};

export default IncorrectUseRef;

在上述错误示例中,由于 showTextRef 使用 useRef 保存值,点击按钮改变 showTextRef.current 不会触发组件重新渲染,所以 Text should show or hide 不会按预期显示或隐藏。正确的做法是使用 useState 来保存这个状态。

import React, { useState } from'react';

const CorrectUseState = () => {
  const [showText, setShowText] = useState(false);
  const toggleText = () => {
    setShowText(!showText);
  };

  return (
    <div>
      <button onClick={toggleText}>Toggle Text</button>
      {showText && <p>Text should show or hide</p>}
    </div>
  );
};

export default CorrectUseState;

在这个正确示例中,使用 useState 保存状态,状态变化会触发组件重新渲染,从而正确地显示或隐藏文本。

  1. 在大型应用中的性能考量 在大型 React 应用中,useRef 的使用应该更加谨慎。因为大量的 useRef 如果使用不当,可能会导致内存泄漏或者难以维护的代码结构。例如,如果在一个组件树的深层组件中过度使用 useRef 来保存全局相关的数据,可能会导致数据的流向不清晰,并且难以追踪数据的变化。

我们应该遵循 React 的设计原则,尽量通过状态提升或者使用更合适的状态管理工具(如 Redux 或 MobX)来管理应用的状态,而 useRef 更多地用于局部的、与 DOM 操作或特定组件内部的可变数据保存相关的场景。

同时,在大型应用中进行性能优化时,要结合 React DevTools 等工具来分析 useRef 的使用情况,确保其使用是合理且不会对性能造成负面影响。

通过深入理解 useRef Hook 的多种用途以及在性能优化方面的注意事项,我们能够在 React 开发中更加灵活和高效地使用它,编写出更健壮、性能更优的前端应用程序。无论是在简单的 DOM 操作,还是复杂的组件交互和状态管理场景中,useRef 都为我们提供了强大而灵活的功能。