Featured image of post 深入探究 React: 通过真实案例理解 useEffect, useRef, useMemo 和 useState
React 前端开发 Web 性能 JavaScript 编程 技术写作

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

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

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 冒险才刚刚开始, 继续探索, 保持好奇, 每一步都会让你看得更清楚。

© 2022 - 2026 张欣耕

保留所有权利