深入探究 React: 通过真实案例理解 useEffect, useRef, useMemo 和 useState

深入探究 React: 通过真实案例理解 useEffect, useRef, useMemo 和 useState

通过实际场景和详细讲解, 探索 React Hooks 的奥秘. 深入理解如何高效使用 useEffect, useRef, useMemo 和 useState, 打造健壮且高性能的 React 应用.

阅读时长: 6 分钟

React 有点像探索一座古老的城堡——迷人, 充满隐藏的房间, 有时还让人感到有点神秘。你一开始信心满满, 以为自己已经很熟悉了。但突然间, 你可能会遇到奇怪的 bug, 消失的事件监听器, 或者因为副作用导致的无限循环。

今天, 让我们一起开启一段 React 的探险之旅。我们将通过一个实际的示例脚本, 逐层揭开 useEffect, useRef, useMemouseState 这些 Hook 的神秘面纱。我们会深入了解这些 Hook 如何与 DOM 交互, 掌握它们的生命周期, 并发现它们的实现方式如何影响你的应用性能。

装备好你的行囊, 冒险者——我们要深入探索了!


示例脚本: 冒险的舞台

这是我们的冒险 React 组件:

import { useEffect, useRef, useState, useMemo } from "react";

function AdventureComponent() {
  const [inputValue, setInputValue] = useState("");
  const [count, setCount] = useState(0);
  const inputRef = useRef(null);

  const expensiveCalculation = useMemo(() => {
    console.log("Calculating heavily...");
    return count * 100;
  }, [count]);

  useEffect(() => {
    const input = inputRef.current;
    const handleFocus = () => console.log("Input Focused");

    input.addEventListener("focus", handleFocus);

    return () => input.removeEventListener("focus", handleFocus);
  }, []);

  useEffect(() => {
    const intervalId = setInterval(() => {
      console.log("Interval tick:", inputValue);
    }, 1000);

    return () => clearInterval(intervalId);
  }, [inputValue]);

  return (
    <div>
      <input ref={inputRef} value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <div>Count: {count}</div>
      <div>Expensive Value: {expensiveCalculation}</div>
    </div>
  );
}

接下来, 让我们逐一揭示每个决策, Hook 以及潜在的陷阱。


useState: 神奇的变量守护者

作用

useState 让你的 React 组件拥有内部状态。每当状态变化时, 它会触发组件重新渲染。

脚本中的示例

const [inputValue, setInputValue] = useState("");
const [count, setCount] = useState(0);

当你调用 setInputValuesetCount 时, React 会安排一次重新渲染。这让你的 UI 保持响应和最新。

注意事项

  • 频繁的状态更新: 过于频繁的状态更新会导致性能下降, 因为会引发过多的渲染。

useRef: 时光穿梭的引用

作用

useRef 提供了一个稳定的引用, 可以指向 DOM 节点或在组件整个生命周期内持久存在的值, 但不会引起组件重新渲染。

脚本中的示例

const inputRef = useRef(null);

<input ref={inputRef} />;

最初, inputRef.currentnull。当 React 渲染完 input 元素后, inputRef.current 就会指向真实的 DOM 节点。

生命周期

  • 初始时: inputRef.current 为 null。
  • 首次渲染后: 引用实际的 DOM 元素。
  • 组件卸载时: React 会自动清理, ref 会变回 null。

注意事项

  • 直接操作 DOM: 只有在 React 不管理该部分时才安全。通过 ref 绑定监听器时要确保不会操作过时的 DOM 节点。

useEffect: 副作用的掌控者

作用

用于处理副作用, 比如事件监听, 网络请求或定时器。默认在每次渲染后执行, 除非依赖数组另有规定。

脚本中的示例

useEffect(() => {
  const input = inputRef.current;
  const handleFocus = () => console.log("Input Focused");

  input.addEventListener("focus", handleFocus);

  return () => input.removeEventListener("focus", handleFocus);
}, []);

由于依赖数组为空, 这段代码只会在初次渲染后执行一次, 把事件监听器绑定到 input 上。

清理模式

  • React 会在重新执行 effect 或组件卸载前, 调用返回的清理函数。务必在这里移除监听器或定时器, 避免内存泄漏。

注意事项

  • 无限循环: 忽略或错误使用依赖项可能导致副作用无限循环执行。
  • 闭包过时: 忘记添加依赖项可能导致 effect 使用了过期的值。

useMemo: 记住昂贵的计算结果

作用

缓存耗时的计算结果, 仅在依赖项变化时重新计算。

脚本中的示例

const expensiveCalculation = useMemo(() => {
  console.log("Calculating heavily...");
  return count * 100;
}, [count]);

只有当 count 变化时, 这个计算才会重新执行。否则, React 会复用缓存的值, 节省 CPU 资源。

注意事项

  • 过度使用: 滥用 useMemo 反而可能带来额外开销, 而不是优化。

原生 DOM API 与 React: 朋友还是对手?

原生 DOM 方法 (如 document.getElementById, addEventListener) 很强大, 但 React 有自己的一套 DOM 管理方式。

何时安全

  • 全局事件 (如 window resize)
  • 通过 ref 绑定在稳定, 非 React 管理的 DOM 节点上的事件监听

何时危险

  • 直接操作由 React 管理的 DOM 元素 (可能导致不一致和 bug)

示例 (危险用法)

useEffect(() => {
  const el = document.getElementById("my-input");
  el.addEventListener("focus", handler);
}, []);

如果 React 之后替换了该 DOM 节点, 你的事件监听器就会失效, 导致难以发现的 bug。


性能与优化建议

  • useEffectuseMemo 中始终正确填写依赖数组。
  • 尽量减少状态更新, 能批量更新时就批量。
  • useRef 避免不必要的重新渲染。

总结: 你的 React 冒险才刚刚开始

深入理解 React Hooks, 就像拥有了一张可靠的冒险地图。它能帮助你解决 bug, 优化性能, 并写出简洁, 健壮的代码。

记住, React 并不是魔法——它只是设计得很巧妙。当你掌握了这些细节后, 你会发现 React 既有趣又不再神秘。你的 React 冒险才刚刚开始, 继续探索, 保持好奇, 每一步都会让你看得更清楚。