揭开 useRef 的神秘面纱:深度剖析及有趣示例!

455 阅读7分钟

引言

在 React 开发过程中,useStateuseEffect 是开发者们常用的两个 Hooks。然而,当我们需要在事件监听器或定时器回调中获取最新的状态值时,useStateuseEffect 可能会显得力不从心。此时,useRef 作为一个能够持久保存引用的 Hook,便显得尤为重要。本文将深度剖析 useRef 的原理,帮助你更好地理解其背后的工作机制。

一、useRef 的原理

1.1. 基本概念

useRef 是 React 提供的一个 Hook,用于在函数组件中创建一个持久的引用。它的工作原理非常简单:返回一个包含 current 属性的对象,这个对象在组件的整个生命周期内保持不变。我们可以通过读取和修改 current 属性来存储和访问任意值。

const myRef = useRef(initialValue);
  • myRef 是一个包含 current 属性的对象。
  • initialValueref 对象的初始值。
  • myRef.current 可以读取和修改。

1.2. useRef 与 useState 的区别

  1. 重新渲染useState 更新状态时会引起组件重新渲染,而 useRef 不会。因此,useRef 更适合保存那些不需要引起组件重新渲染的可变值。
  2. 持久性useRef 创建的引用在组件的整个生命周期内保持不变,即使组件重新渲染,ref 对象也不会改变。而 useState 每次渲染都会重新创建。

1.3. React 组件渲染流程

为了更好地理解 useRef 的原理,我们需要简单回顾一下 React 组件的渲染流程。每次组件重新渲染时,React 会重新执行组件函数,重新计算 JSX,并生成新的虚拟 DOM。这意味着组件内的局部变量会被重新初始化,而 useRef 创建的引用对象则在组件的整个生命周期内保持不变。

1.4. useRef 内部实现

useRef 的内部实现相对简单。它返回一个包含 current 属性的对象,这个对象在整个组件生命周期中保持一致。React 内部通过一个称为“hook 链表”的数据结构来管理 Hooks,每次渲染时,React 都会遍历这个链表,从而确保 Hooks 的顺序和状态的一致性。

示例实现(简化版)

以下是一个简化版的 useRef 实现,用于帮助理解其工作机制:

let currentHook = null;

function useRef(initialValue) {
  if (!currentHook) {
    currentHook = { current: initialValue };
  }
  return currentHook;
}

在实际的 React 实现中,Hooks 是通过一个链表来管理的,useRef 会返回链表中的一个节点,这个节点在整个组件生命周期中保持不变。

1.5. useRef 与闭包

闭包是 JavaScript 中的一个重要概念,它允许函数访问其词法作用域中的变量。在 React 中,闭包也起到了关键作用。当我们在函数组件中定义一个函数时,这个函数会捕获其所在作用域中的变量。这意味着当我们在 useEffect 中定义一个事件处理函数时,这个函数会捕获当时的状态值,而不会随着状态的变化而更新。

闭包示例

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

const Counter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const handleClick = () => {
      console.log(count);
    };
    
    window.addEventListener('click', handleClick);
    return () => {
      window.removeEventListener('click', handleClick);
    };
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default Counter;

在这个例子中,handleClick 函数捕获了初始的 count 值(0),即使 count 发生变化,handleClick 函数中访问到的 count 仍然是最初的值。这就是闭包的工作原理。

1.6. useRef 解决闭包问题

为了在事件处理函数中访问最新的状态值,我们可以使用 useRef 来存储这个状态值,并在每次状态更新时同步更新 ref 对象。

解决方案

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

const Counter = () => {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  useEffect(() => {
    const handleClick = () => {
      console.log(countRef.current);
    };

    window.addEventListener('click', handleClick);
    return () => {
      window.removeEventListener('click', handleClick);
    };
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default Counter;

在这个例子中,每次 count 更新时,通过一个 useEffect 将最新的 count 值同步到 countRef.current 中。这样,事件处理函数 handleClick 就可以访问到最新的 count 值。

二、useRef 的应用场景

2.1. 访问 DOM 元素

useRef 最常见的用途之一是访问 DOM 元素。在类组件中,我们可以使用 React.createRef() 创建一个 ref,在函数组件中则可以使用 useRef

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

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

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

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

export default InputFocus;

在这个例子中,我们使用 useRef 创建了一个 inputRef,并将其赋值给 <input> 元素的 ref 属性。通过 useEffect,在组件挂载后立即调用 inputRef.current.focus(),使得输入框自动获得焦点。

2.2. 保存可变的值

如前文所述,useRef 可以用来保存一个可变的值,这个值在组件的整个生命周期内保持不变,并且不会触发组件的重新渲染。

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

const Stopwatch = () => {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  const start = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
  };

  const stop = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  };

  const reset = () => {
    stop();
    setSeconds(0);
  };

  return (
    <div>
      <h1>{seconds}</h1>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
};

export default Stopwatch;

在这个例子中,我们用 useRef 保存了一个定时器的 ID,通过 intervalRef.current 来判断定时器是否已经启动,从而避免重复启动定时器。

2.3. 与事件监听器配合使用

在某些场景下,我们需要在事件监听器中访问最新的状态值。这时可以通过 useRef 来保存最新的状态。

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

const ClickCounter = () => {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  useEffect(() => {
    const handleClick = () => {
      alert(`Current count: ${countRef.current}`);
    };

    document.addEventListener('click', handleClick);
    
    return () => {
      document.removeEventListener('click', handleClick);
    };
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default ClickCounter;

在这个例子中,每当组件的 count 状态更新时,通过 useEffectcount 的最新值保存到 countRef.current。这样,在事件监听器 handleClick 中就可以访问到最新的 count 值。

2.4. 缓存函数

有时我们需要缓存一个函数,以避免其在每次渲染时都被重新创建。这时可以使用 useRef 来保存函数引用。

import React, { useRef, useCallback } from 'react';

const FunctionCache = () => {
  const renderCount = useRef(0);

  const cachedFunction = useCallback(() => {
    console.log('This function is cached');
  }, []);

  renderCount.current += 1;

  return (
    <div>
      <p>Render count: {renderCount.current}</p>
      <button onClick={cachedFunction}>Call Cached Function</button>
    </div>
  );
};

export default FunctionCache;

在这个例子中,我们使用 useCallback 创建了一个缓存的函数,并通过 useRef 记录组件的渲染次数。每次点击按钮时,cachedFunction 都不会被重新创建。

2.5. 保存上一次渲染的值

有时我们需要访问组件上一次渲染时的某个值,这可以通过 useRef 实现。

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

const PreviousValue = () => {
  const [value, setValue] = useState('');
  const previousValueRef = useRef('');

  useEffect(() => {
    previousValueRef.current = value;
  }, [value]);

  return (
    <div>
      <p>Current value: {value}</p>
      <p>Previous value: {previousValueRef.current}</p>
      <input 
        type="text" 
        value={value} 
        onChange={(e) => setValue(e.target.value)} 
      />
    </div>
  );
};

export default PreviousValue;

在这个示例中,每当 value 更新时,通过 useEffect 将当前值保存到 previousValueRef.current,从而在下一次渲染时可以访问到上一次的值。

2.6. 记录渲染次数

通过 useRef,我们可以轻松记录组件的渲染次数。

import React, { useRef } from 'react';

const RenderCounter = () => {
  const renderCount = useRef(0);
  renderCount.current += 1;

  return (
    <div>
      <p>Render count: {renderCount.current}</p>
    </div>
  );
};

export default RenderCounter;

在这个示例中,每次组件渲染时,renderCount.current 都会增加 1,从而记录组件的渲染次数。

2.7. 防抖 (Debounce) 功能

我们可以使用 useRef 来实现防抖功能,以避免某些操作被频繁触发。

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

const DebouncedInput = () => {
  const [value, setValue] = useState('');
  const debounceRef = useRef(null);

  const handleChange = (e) => {
    const newValue = e.target.value;
    if (debounceRef.current) {
      clearTimeout(debounceRef.current);
    }
    debounceRef.current = setTimeout(() => {
      setValue(newValue);
    }, 300);
  };

  return (
    <div>
      <input 
        type="text" 
        onChange={handleChange} 
      />
      <p>Debounced value: {value}</p>
    </div>
  );
};

export default DebouncedInput;

在这个示例中,每当输入框的内容变化时,通过 useRef 保存一个定时器 ID,实现防抖功能,从而避免输入框内容频繁变化时频繁更新状态。

总结

通过本文的深入解析,我们了解了 useRef 的工作原理及其在 React 开发中的应用场景。从访问 DOM 元素、保存可变的值,到与事件监听器配合使用,再到缓存函数,useRef 展现了其强大的功能。我们还通过多个有趣的示例展示了 useRef 的灵活性和实用性。

在实际开发中,useRef 是一个非常有用的工具,可以帮助我们解决许多状态管理和性能优化的问题。希望本文能够帮助你更好地理解和应用 useRef,提升你的 React 开发技能。