【React Hook全家桶】大致过一遍React Hooks

React Hook 全家桶核心逻辑(最底层原理)

所有 Hook 都存在组件的链表 (Hook Chain) 中,React 根据调用顺序来读取状态。

1.useState和useEffect:

1.1 useState 和 useEffect 的执行时机

关键时间线:

javascript 复制代码
function Component() {
  console.log('1. 组件函数执行');
  
  const [count, setCount] = useState(0);
  console.log('2. useState 返回值');
  
  useEffect(() => {
    console.log('4. useEffect 执行(DOM 已更新)');
    return () => console.log('5. 清理函数(下次 effect 前或卸载时)');
  }, [count]);
  
  console.log('3. return JSX 前');
  return <div>{count}</div>;
}

// 输出顺序:1 → 2 → 3 → 渲染 DOM → 4

浏览器永远是:

  1. 先执行完所有同步 JS 代码
  2. 再执行一次浏览器渲染(解析HTML+CSS+合成渲染树+布局 + 绘制)
  3. 最后才执行异步队列(微任务 → 宏任务)

并且:

  • useEffect 属于异步任务
  • DOM 渲染在同步代码之后
时间线:
javascript 复制代码
同步 JS 执行阶段(阻塞渲染)
  1. 组件执行
  2. useState
  3. 注册 useEffect(不执行)
  4. return JSX
同步代码执行完毕 

浏览器渲染阶段(JS 暂停)
  5. React 更新真实 DOM
  6. 浏览器布局、绘制
DOM 渲染完毕 

异步任务阶段
  7. 执行 useEffect 回调

实战案例:聊天室滚动到底部

javascript 复制代码
function ChatRoom({ messages }) {
  const bottomRef = useRef(null);
  
  useEffect(() => {
    // √此时 DOM 已渲染,可以安全操作
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]); // 新消息到达时滚动
  
  return (
    <div>
      {messages.map(msg => <div key={msg.id}>{msg.text}</div>)}
      <div ref={bottomRef} />
    </div>
  );
}

常见错误:

javascript 复制代码
// 错误:在渲染阶段操作 DOM
function Bad() {
  const ref = useRef(null);
  ref.current?.focus(); // 此时 DOM 可能还没渲染!
  return <input ref={ref} />;
}

// 正确:在 useEffect 中操作
function Good() {
  const ref = useRef(null);
  useEffect(() => {
    ref.current?.focus(); // DOM 已就绪
  }, []);
  return <input ref={ref} />;
}

2. useRef:稳定引用与缓存

  • 核心 :返回一个永远不变 的对象 { current: 值 }
  • 特性 :修改 .current 不会触发组件重渲染。
  • 本质 :它是类组件 this 的替代方案,专门用于存值 而不是渲染
  • 两大场景
    1. 获取 DOM 节点ref={inputRef}
    2. 存储可变值 :定时器 ID、上一次的 props/state(通过 .current 存取以避开闭包陷阱)。

3. useRef vs useState:什么时候用谁?

核心区别:

特性 useState useRef
改变时重新渲染 是的 不是
保存跨渲染的值 是的 是的
适用场景 UI 状态 DOM 引用、定时器 ID、上一次的值

实战案例 1:保存定时器 ID

javascript 复制代码
function Stopwatch() {
  const [time, setTime] = useState(0);
  const timerRef = useRef(null); // 不需要触发渲染
  
  const start = () => {
    timerRef.current = setInterval(() => {
      setTime(t => t + 1);
    }, 1000);
  };
  
  const stop = () => {
    clearInterval(timerRef.current); // 访问最新的 timer ID
  };
  
  useEffect(() => {
    return () => clearInterval(timerRef.current); // 清理
  }, []);
  
  return (
    <div>
      <p>{time}s</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </div>
  );
}

为什么不用 useState 存 timer ID?

javascript 复制代码
// ❌ 错误示范
const [timerId, setTimerId] = useState(null);

const start = () => {
  setTimerId(setInterval(...)); // 触发重新渲染,浪费性能
};

只有这 3 种东西能触发组件重渲染

  1. useState 里的 setXxx
  2. useReducer 的 dispatch
  3. 父组件重新渲染

👉 useEffect 绝对不能触发重渲染! 它只是个副作用回调,本身不触发渲染。

  • setState 调用 → 一定重渲染(组件函数跑一遍)
  • 值真的变了 ( React 用 Object.is 对比:值变化)→ 才更 DOM→ 才重新执行useEffect
  • 值没变 → 组件重跑,但 DOM 不动(React 优化),也不重新执行useEffect

实战案例 2:保存上一次的值

javascript 复制代码
function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value; // 渲染后更新
  });
  
  return ref.current; // 返回上一次的值
}

// 使用
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  
  return (
    <div>
      <p>当前: {count}</p>
      <p>上一次: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

4. useMemo & useCallback:缓存优化(语法糖关系)

两者底层逻辑一致,都是基于**Object.is** 做浅比较缓存。

  • useMemo :缓存计算结果(值)
    • 作用:避免复杂逻辑在每次渲染时重复执行(如大数据过滤)。
    • 公式:const value = useMemo(() => calc(bigList), [bigList])
  • useCallback :缓存函数引用(函数本身)
    • 作用:保证函数地址不变,防止子组件(被 memo 包装)不必要的重渲染。
    • 本质:它就是 useMemo(() => () => {}, []) 的语法糖。
    • 公式:const fn = useCallback(() => {}, [])
javascript 复制代码
// 等价于
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);

什么时候用?什么时候不用?

应该用的场景:

场景 1:昂贵的计算

javascript 复制代码
function ProductList({ products, filters }) {
  // ✅ 过滤和排序很耗时,缓存结果
  const filteredProducts = useMemo(() => {
    console.log('重新计算...');
    return products
      .filter(p => p.category === filters.category)
      .sort((a, b) => b.price - a.price);
  }, [products, filters]);
  
  return (
    <ul>
      {filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

场景 2:避免子组件不必要的重新渲染

javascript 复制代码
const ExpensiveChild = React.memo(({ data, onUpdate }) => {
  console.log('子组件渲染');
  return <div>{data.name}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: 'Alice' });
  
  // ✅ 缓存函数,避免每次渲染创建新函数
  const handleUpdate = useCallback(() => {
    setUser({ ...user, name: 'Bob' });
  }, [user]);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      {/* count 变化时,ExpensiveChild 不会重新渲染 */}
      <ExpensiveChild data={user} onUpdate={handleUpdate} />
    </div>
  );
}
  • 父组件渲染 → 默认所有子组件都会跟着渲染
  • React.memo 让子组件变成 "浅比较 props",只有 props 变了才渲染
  • 但函数是引用类型,每次父组件重渲染都会创建新函数 → 导致 memo 失效
  • useCallback 就是用来 "缓存函数",让函数引用不变 → memo 才能生效
  • 最终:count 变化时,子组件不渲染!
不应该用的场景:
javascript 复制代码
// ❌ 过度优化:简单计算不需要 useMemo
const doubled = useMemo(() => count * 2, [count]); // 浪费
const doubled = count * 2; // 直接算更快

// ❌ 依赖项太多,缓存失效频繁
const result = useMemo(() => a + b + c + d + e, [a, b, c, d, e]); // 没意义

// ❌ 子组件没用 React.memo,缓存函数无效
function Child({ onClick }) { // 没有 React.memo
  return <button onClick={onClick}>点击</button>;
}

function Parent() {
  const handleClick = useCallback(() => {}, []); // 白费力气
  return <Child onClick={handleClick} />;
}

5. memo:组件层面的门禁

  • 作用 :包裹函数组件,默认阻止重渲染。
  • 逻辑 :只有传给子组件的 props 发生变化(Object.is 对比),子组件才会重新渲染。
  • 配合 :必须和 useCallback 配合使用,否则函数引用变化依然会触发子组件重渲染。

6. 自定义 Hook:封装可复用逻辑

实战案例 1:useDebounce(防抖值)
javascript 复制代码
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => clearTimeout(timer);
  }, [value, delay]);
  
  return debouncedValue;
}

// 使用
function SearchBox() {
  const [input, setInput] = useState('');
  const debouncedInput = useDebounce(input, 500);
  
  useEffect(() => {
    if (debouncedInput) {
      fetch(`/api/search?q=${debouncedInput}`)
        .then(res => res.json())
        .then(setResults);
    }
  }, [debouncedInput]); // 只在防抖后的值变化时请求
  
  return <input value={input} onChange={e => setInput(e.target.value)} />;
}
  1. 用户输入 → input 立刻更新
  2. useDebounce 监测到 input 变化
  3. 开启 500ms 定时器
  4. 500ms 内再次输入 → 清除旧定时器,重新计时
  5. 停手 500ms → debouncedInput 才更新
  6. debouncedInput 变化 → 发送搜索请求

**只发一次请求,完美避免频繁请求!**useDebounce = 让值延迟更新,专门解决输入搜索、频繁请求的防抖问题,是 React 最实用的自定义 Hook 之一。

实战案例 2:useLocalStorage(持久化状态)
javascript 复制代码
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });
  
  const setStoredValue = useCallback((newValue) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  }, [key]);
  
  return [value, setStoredValue];
}

// 使用,主题变化:
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      当前主题: {theme}
    </button>
  );
}

逐行拆解:

javascript 复制代码
function useLocalStorage(key, initialValue) {
  // 1. 初始化:优先从 localStorage 取,取不到就用默认值
  const [value, setValue] = useState(() => {
    try {
      const item = localStorage.getItem(key); // 从本地存储拿数据
      return item ? JSON.parse(item) : initialValue; // 有就解析,没有就用默认值
    } catch {
      return initialValue; // 解析失败也用默认值
    }
  });

  // 2. 缓存一个更新函数:改 state 同时自动存 localStorage
  const setStoredValue = useCallback((newValue) => {
    setValue(newValue); // 先更新内部 state
    localStorage.setItem(key, JSON.stringify(newValue)); // 再同步存到本地
  }, [key]); // 依赖 key,key 不变函数就不变

  // 3. 返回:用法和 useState 一毛一样!
  return [value, setStoredValue];
}

完整执行流程:

我用主题切换的例子走一遍:

第一次进入页面(初始化):

  1. 执行 useLocalStorage('theme', 'light')
  2. 进入 useState 初始化函数
  3. 去 localStorage 找 theme 这个 key
    • 第一次:找不到
    • 所以 value = 'light'
  4. 准备好 setStoredValue 函数
  5. 返回 ['light', 函数]

点击按钮切换主题:

  1. 执行 setTheme('dark')
  2. 调用 setStoredValue('dark')
  3. 内部执行:
    • setValue('dark') → 状态更新
    • localStorage.setItem('theme', 'dark')存到本地
  4. 页面重渲染 → 显示 dark

刷新页面!(最关键):

  1. 再次执行 useLocalStorage('theme', 'light')
  2. 去 localStorage 找 theme
    • 找到了!值是 'dark'
  3. JSON.parse 解析后返回
  4. value = 'dark'
  5. 返回 ['dark', ...]
  6. 页面直接显示 dark,不会变回 light!

这就是持久化状态。

原生 localStorage useLocalStorage Hook
是什么 浏览器原生存储 API React 自定义 Hook
是否响应式 不触发渲染 自动触发渲染
是否自动同步 手动存、手动取 改状态自动存
JSON 处理 自己写 自动处理
异常处理 容易崩 自带 try/catch
适合谁 原生 JS、简单场景 React 项目、状态持久化
实战案例 3:useIntersectionObserver核心hook:
javascript 复制代码
function useIntersectionObserver(ref, options) {
  const [isVisible, setIsVisible] = useState(false);
  
  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      setIsVisible(entry.isIntersecting);
    }, options);
    
    if (ref.current) {
      observer.observe(ref.current);
    }
    
    return () => observer.disconnect();
  }, [ref, options]);
  
  return isVisible;
}
懒加载:监听图片是否可见
javascript 复制代码
function LazyImage({ src }) {
  const imgRef = useRef(null);
  const isVisible = useIntersectionObserver(imgRef, { threshold: 0.1 });
  
  return (
    <div ref={imgRef}>
      {isVisible ? <img src={src} /> : <div>加载中...</div>}
    </div>
  );
}
无限滚动组件:

思路:页面底部放一个看不见的小 div → 它一出现就加载下一页

javascript 复制代码
function InfiniteList() {
  const [list, setList] = useState([]);
  const [page, setPage] = useState(1);
  const loading = useRef(false);

  // 底部的"加载触发器"
  const loadMoreRef = useRef(null);
  const isVisible = useIntersectionObserver(loadMoreRef, {
    threshold: 0.1,
  });

  // 模拟加载数据
  const loadMore = useCallback(async () => {
    if (loading.current) return;
    loading.current = true;

    // 模拟接口请求
    const newData = await fetchList(page);

    setList(prev => [...prev, ...newData]);
    setPage(prev => prev + 1);
    
    loading.current = false;
  }, [page]);

  // 监听底部元素是否出现 → 出现就加载
  useEffect(() => {
    if (isVisible) {
      loadMore();
    }
  }, [isVisible, loadMore]);

  return (
    <div>
      {/* 列表 */}
      {list.map(item => (
        <div key={item.id}>{item.content}</div>
      ))}

      {/* 👇 这就是无限滚动的"触发器" */}
      <div ref={loadMoreRef} style={{ height: 10 }} />
      
      {loading.current && <p>加载中...</p>}
    </div>
  );
}
执行流程:
  1. 页面渲染,展示第一页数据
  2. 你往下滚动
  3. 快到底部时,loadMoreRef 这个 div 进入屏幕
  4. isVisible 变成 true
  5. useEffect 监听到 isVisible 变化
  6. 自动调用 loadMore()
  7. 加载新数据,拼接到列表
  8. 列表变长,继续滚动 → 循环

为什么这样写超级强?
  • 不用监听 scroll(性能爆炸好)
  • 不用算距离、不用算高度
  • 浏览器原生优化,丝滑不卡顿
  • 逻辑干净,不容易出 bug
  • 懒加载、无限滚动 一套 Hook 通用

绝对不能踩的致命坑:

坑 1:数组变异方法 setState(push 陷阱)
  • 错误setState(arr.push(newItem))
    • 原因push 返回的是新长度(数字),不是新数组。
    • 后果 :state 变成数字,后续 .map 直接报错。
    • 原则 :React 状态必须是不可变数据 (Immutable)
  • 正确setState([...arr, newItem])(扩展运算符生成新数组)。
  • 进阶setState(prev => [...prev, newItem])(函数式更新,依赖前一个状态)。
坑 2:useCallback 空依赖陷阱
  • 现象useCallback(() => { console.log(count) }, [])
  • 问题 :空依赖会永久捕获 首次渲染的变量值。即使 count 变了,函数里拿到的永远是初始值(闭包陷阱)。
  • 解法
    1. 把变量加入依赖数组 [count]
    2. 或用 useRef 存储最新值中转。
坑 3:memo 与普通函数的区别
  • 现象 :子组件加了 memo,但点击父组件状态还是重渲染了。
  • 原因 :父组件每次渲染都会重新创建函数,导致传给子组件的 props 引用变了。
  • 解法 :函数必须套上 useCallback,保证引用稳定。

黄金组合公式(开发必背)

想要实现高性能子组件,必须组合使用:

  1. 子组件 :用 memo 包裹。

    javascript 复制代码
    const Child = memo(({ onClick }) => <button onClick={onClick}>按钮</button>);
  2. 父组件 :函数用 useCallback 缓存。

    javascript 复制代码
    const handleClick = useCallback(() => { /* 逻辑 */ }, [依赖]);
  3. 传参 :将缓存后的函数传给子组件。

    javascript 复制代码
    <Child onClick={handleClick} />

总结与记忆口诀

  1. 存值不重渲染用 useRef
  2. 缓存计算值用 useMemo
  3. 缓存函数用 useCallback
  4. 组件门禁用 memo
  5. setState 永远用新引用,拒绝 push。
相关推荐
sheji34162 小时前
【开题答辩全过程】以 基于web的图书借阅系统的设计与实现为例,包含答辩的问题和答案
前端
●VON2 小时前
Flutter组件深度解析:从基础到高级的完整指南
android·javascript·flutter·harmonyos·von
CodeSheep2 小时前
两位大佬相继离世,AI时代我们活得太着急了
前端·后端·程序员
xuankuxiaoyao2 小时前
VUE.JS 实践 第三章
前端·javascript·vue.js
放下华子我只抽RuiKe52 小时前
NLP自然语言处理硬核实战笔记
前端·人工智能·机器学习·自然语言处理·开源·集成学习·easyui
PieroPc2 小时前
电脑DIY组装报价系统 用MiMo V2 Pro 写html ,再用opencode(选MiMo 作模型) 当录入口
前端·html
工程师老罗2 小时前
lvgl有哪些布局?
前端·javascript·html
好家伙VCC2 小时前
# 发散创新:用Selenium实现自动化测试的智能断言与异常处理策略在现代Web应用开发中,*
java·前端·python·selenium
木子清billy2 小时前
物联网浏览器(IoTBrowser)-js开发人脸识别
开发语言·javascript·物联网