1. 什么是 React Hooks?为什么出现?
Hooks 是 React 16.8 引入的特性,让函数组件也能使用 state 和生命周期逻辑。
Hooks 的出现解决了 class 组件代码容易臃肿的问题,比如一个生命周期里写了很多不相关逻辑,逻辑复用只能靠 HOC 或 Render Props,写起来嵌套复杂。
而 Hooks 能将内部逻辑抽出来复用,不用像以前那样需要写一堆 class 组件。
2. React Hooks 的执行顺序和依赖规则
执行顺序
Hooks 是按 读代码的顺序 记录和重用的——必须在函数最顶层调用(不能在 if/loop/嵌套函数里)。
React 会根据这种顺序把状态、effect、refs 绑定到组件上;每次渲染 React 都“按顺序”走一遍并复用之前的 Hook 槽位。
依赖规则
- 无依赖数组
useEffect(() => {
console.log('每次渲染都执行');
});
每次渲染后都执行。
- 空依赖数组
useEffect(() => { fetchData(); }, []); // 空数组
仅在首次渲染后执行一次(组件挂载时)。
- 包含依赖项
useEffect(() => { fetchUser(userId); }, [userId]); // 依赖 userId
userId 变化时执行(响应特定数据变化)。
3. 为什么 Hooks 不能写在条件语句或循环里?
因为 React 是靠调用顺序来匹配每个 Hook 的。 React 其内部有一个 Hook 链表,每次渲染都会顺序调用。如果在条件语句里写 Hook,下一次渲染可能顺序就乱了,导致 React 取错值。 所以 React 就干脆禁止了这种写法。
4. useState 的作用?state 更新是同步还是异步?
定义:useState 用来在函数组件中存储和更新状态。
useState更新是异步的,React 会合并多个 state 更新,一次性触发渲染。在React 18 以后在事件中是自动批量更新,避免了多次渲染浪费。
5. useEffect 和 useLayoutEffect 有什么区别?(useEffect 和 useLayoutEffect 的执行时机有何不同)?
useEffect在渲染完成后执行useLayoutEffect在 DOM 更新后、浏览器绘制前执行
使用场景:
useEffect→ 非阻塞,适合异步操作(请求、日志)useLayoutEffect→ 会阻塞绘制,适合同步 DOM 操作
选取思路:
- 默认选择
useEffect,适用于 99% 的副作用场景,性能最优 - 当出现视觉闪烁、布局跳动时考虑
useLayoutEffect useLayoutEffect中操作要轻量,仅进行必要的 DOM 修改
6. useMemo 和 useCallback 的区别?什么时候用?如果滥用会怎么样?
useMemo
缓存计算结果,避免重复计算开销大的值
import { useMemo } from "react";
function MyComponent({ items }) {
// 假设有一个计算密集型函数
const computeExpensiveValue = (list) => {
console.log("计算中...");
return list.reduce((sum, item) => sum + item.value, 0);
};
// useMemo 用法
const total = useMemo(() => computeExpensiveValue(items), [items]);
return (
<div>
<p>Total: {total}</p>
</div>
);
}
- 每次渲染时,如果
items没变,React 会直接用上一次的total,不会重复执行computeExpensiveValue。 - 实战场景:列表排序、筛选、统计、图表数据处理,或者任何需要“重计算”的场景。
useCallback
缓存函数引用,避免子组件重复渲染
import { useState, useCallback } from "react";
function Parent() {
const [count, setCount] = useState(0);
// 普通函数:每次渲染都会创建新函数
// const handleClick = () => setCount(count + 1);
// useCallback 版:使用函数式更新,避免依赖 count
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // 依赖数组为空,函数引用永远不变
return (
<div>
<p>Count: {count}</p>
<Child onClick={handleClick} />
</div>
);
}
const Child = React.memo(({ onClick }) => {
console.log("Child 渲染");
return <button onClick={onClick}>+1</button>;
});
Child用React.memo包裹,如果onClick函数每次渲染都变,Child会重复渲染。改用useCallback缓存函数引用,Child就不会重复渲染了。- 常用于父组件状态频繁变化但子组件不想重渲染,比如列表中的按钮、表格操作列、卡片组件等。
滥用的后果 1.内存开销 增加每个 useMemo/useCallback 都需要内存来存储缓存 2.初始渲染性能下降 React 需要额外的工作来管理这些缓存,对于简单计算,缓存的开销可能比计算本身还大 3.代码复杂度增加 过多的 useMemo 和 useCallback 会降低可读性,需要费心管理依赖数组且容易遗漏依赖
7. useRef 是干嘛的,有哪些常见使用场景?
useRef 会返回一个 可变的对象 { current: ... } ,这个对象在组件的整个生命周期里保持不变,不会因为渲染而重置。
A. 获取 DOM 节点
- 原理:React 会把 DOM 节点挂载到 ref 的
current上。 - 场景:直接操作 DOM,例如自动聚焦、滚动到某个位置或播放视频。
示例:自动聚焦输入框
import { useEffect, useRef } from "react";
function Login() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} placeholder="用户名" />;
}
B. 存储跨渲染的可变变量
- 原理:useRef 返回的对象在多次渲染间保持同一引用,不会触发重新渲染。
- 场景:
-
- 保存定时器 ID、WebSocket 实例等
-
- 保存前一次的状态(prevValue)
-
- 避免闭包问题(stale closure)
示例:保存定时器 ID
import { useEffect, useRef, useState } from "react";
function Timer() {
const timerRef = useRef(null);
const [count, setCount] = useState(0);
useEffect(() => {
timerRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(timerRef.current);
}, []);
return <p>{count}</p>;
}
C. 缓存上一次的值(prevValue)
- 原理:通过 useRef 保存上一次渲染的值,在下一次渲染中访问。
- 场景:对比当前值和上一次值,或者做过渡动画、判断状态变化等。
示例:缓存前一次值
function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; }, [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(c => c + 1)}>+1</button>
</div>
);
}
8. 闭包陷阱
闭包陷阱是指当一个函数在 JavaScript 中“记住”了它创建时的作用域里的变量,但这个变量在后续更新了,函数仍然访问的是旧值,从而出现数据滞后的问题。
即 函数拿到的不是最新的值,而是它第一次闭包里捕获的那个值。
import { useState, useEffect } from "react";
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // ❌ 一直打印 0
setCount(count + 1); // stale closure
}, 1000);
return () => clearInterval(id);
}, []);
}
useEffect 的闭包捕获了 第一次渲染的 count 值(0) ,
后续 state 更新不会改变这个闭包里的 count ,所以计数器无法正确累加
正确写法
A. 函数式更新
setCount(c => c + 1);
- 优点:永远拿到最新值
- 不依赖闭包里的老值
B. useRef 保存最新值
const latestCount = useRef(count);
useEffect(() => { latestCount.current = count; }, [count]);
useEffect(() => {
const id = setInterval(() => {
console.log(latestCount.current);
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
9. 举一个自定义 Hook
自定义 Hook 就是把一些状态逻辑抽出来,形成可复用的函数。
- 比如一个简单的防抖
useDebounce:
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
- 场景:搜索框防抖,避免每次输入都触发请求
10. useContext 怎么用?和 Redux/Zustand 有什么区别?
useContext 用来拿到上层 Provider 提供的数据。
它适用于简单的全局状态,比如主题/语言;缺点是 Provider 值一变,所有子组件都会重渲染。
比如在项目里可以用 useContext 存用户登录信息(userInfo),比起 Redux 简单很多。 而 Redux/Zustand 更适合复杂状态,因为有中间件、持久化和性能优化
11. 什么是useReducer ?和 useContext 的区别?
这两个 Hook 其实解决的不是同一个问题:
- useContext 的作用是数据传递。它能避免一层层传 props,直接把数据分发给子组件。但它本身并不关心数据怎么被修改,所以如果只用 useContext,一般会在里面放一个 state,再通过 setState 去改。这种方式在状态不复杂的时候挺方便,但逻辑会分散在各个组件里。
- useReducer 则是管理数据更新逻辑的。它把 state 的变化统一放在 reducer 函数里,通过 action 来触发更新。这样一来,修改逻辑不会到处乱跑,状态流转路径清晰,尤其在状态复杂、有多种修改方式时更好维护。
所以如果只是单纯对比,useReducer 的优势在于逻辑集中、可预测。
- 用
useReducer统一管理状态和修改逻辑。 - 再用
useContext把 state 和 dispatch 往下传,这样子组件不用层层 props drilling,也不用各自到处写 setState。
可以说,useContext 解决的是“传”的问题,useReducer 解决的是“管”的问题,两者配合起来,就是 React 内置的轻量状态管理方案。