一位初学者的 React Hooks 通关指南:从外卖柜到源码,彻底吃透 useContext 和自定义 Hook
1. 开胃菜:这是啥玩意儿?(用极致白话介绍核心功能)
想象你住在一个超大型小区里(这就是你的 React 应用)。
以前的通信方式(props 逐层传递)
你家住 18 楼,想给 1 楼的物业传达一条信息:"今天晚上我要请客,帮忙留个车位"。按照传统方式,你需要:
- 先把消息告诉 17 楼的邻居
- 17 楼告诉 16 楼
- 16 楼告诉 15 楼
- ... 一直传到 1 楼
问题来了:中间那些楼层根本不需要知道这个消息,但她们不得不当"传话筒"。如果小区有 100 层,这种传递简直要命!
这就是 React 中的 "props drilling"(属性穿透) 问题。
useContext 是什么?------ 小区广播系统 🎤
现在物业在小区装了广播系统(这就是 Context):
- 你在 18 楼对着麦克风说:"我要留车位"
- 所有楼层都能听到(任何组件都能访问)
- 但只有 1 楼的物业真正需要这个信息,其他人可以选择"不听"
核心价值 :跨层级直接通信,不需要中间组件当"传话筒"。
自定义 Hook 是什么?------ 你的"私人工具箱" 🧰
你在小区经常要做三件事:开信箱、拿快递、浇花。每次都要重复"下楼→做事→上楼"这套流程。
你把这三个动作打包成一个"取件浇花套餐"(这就是自定义 Hook):
- 以后只需要说"执行套餐",自动完成所有步骤
- 这个套餐还能在小区任何一栋楼复用
核心价值 :封装可复用的逻辑,把重复代码抽离出来,让组件更干净。
2. 名词解释大全(扫清学习障碍)
🔍 名词 1:Context(上下文)
-
官方定义:Context 提供了一种在组件树中传递数据的方法,无需在每个层级手动传递 props。
-
大白话翻译:就是小区里的"广播系统"。任何组件都能"收听"这个频道,获取共享信息。
-
在代码中如何体现:
javascript
javascript// 这就是"建立广播频道" const ThemeContext = createContext('light'); // 这是"安装广播发射器",所有子组件都能收到信号 <ThemeContext.Provider value={theme}> <Page /> </ThemeContext.Provider> // 这是"打开收音机收听" const theme = useContext(ThemeContext); -
解决了什么痛点 :props drilling(属性穿透) 。如果没有 Context,主题颜色要从 App 传到 GreatGrandChild,需要经过 4 层组件的"手动搬运"。
🔍 名词 2:Provider(提供者)
-
官方定义 :Provider 接收一个
value属性,传递给所有消费组件。每个 Context 对象都有一个 Provider 组件。 -
大白话翻译 :就是广播系统的发射塔 。你往发射塔里放什么数据(
value),所有收听者就能收到什么。 -
在代码中如何体现:
javascript
xml// ThemeContext.Provider 是一个 React 组件 // value 就是"发射的信号",这里传入了 theme 状态 <ThemeContext.Provider value={theme}> <Page /> {/* 所有子孙组件都能收到 theme */} <button onClick={...}>切换主题</button> </ThemeContext.Provider> -
重要细节 :Provider 必须包裹 需要使用数据的组件。如果组件没被 Provider 包裹,
useContext会使用createContext时的默认值。 -
解决了什么痛点 :解决了数据从哪里来的问题。明确指定数据的作用范围------只有被 Provider 包裹的组件树才能访问。
🔍 名词 3:useContext(消费上下文)
-
官方定义:useContext 是一个 Hook,用于在函数组件中读取 Context 的当前值。
-
大白话翻译 :就是打开收音机的按钮。调对频道(传入 Context 对象),就能听到广播内容。
-
在代码中如何体现:
javascript
scss// 在 useTheme 这个自定义 Hook 里 export function useTheme() { return useContext(ThemeContext); // 调频到 ThemeContext 频道 } // 在组件里使用 const theme = useTheme(); // 拿到当前主题值 -
解决了什么痛点 :解决了数据怎么取的问题。不需要关心数据从哪一层传下来的,直接"调频收听"即可。
🔍 名词 4:自定义 Hook(Custom Hook)
-
官方定义 :自定义 Hook 是一个以
use开头的 JavaScript 函数,可以调用其他 Hook,用于封装可复用的逻辑。 -
大白话翻译 :就是你的私人工具箱。把经常要做的"一组操作"打包成一个函数,需要时直接调用。
-
在代码中如何体现:
javascript
javascript// 这是"鼠标追踪器"工具箱 export const useMouse = () => { const [x, setX] = useState(null); const [y, setY] = useState(null); // 封装了"监听鼠标移动并更新坐标"的完整逻辑 useEffect(() => { document.addEventListener("mousemove", handleMouseMove); return () => { document.removeEventListener("mousemove", handleMouseMove); }; }, []); return { x, y }; }; // 在组件中使用:一行代码获得鼠标坐标 const { x, y } = useMouse(); -
和普通函数的区别:
- 普通函数:只能封装逻辑,不能使用 React 的 Hook(如 useState、useEffect)
- 自定义 Hook:可以封装带状态的逻辑,内部可以使用所有 React Hook
-
解决了什么痛点 :解决了逻辑复用 的问题。如果多个组件都需要"获取鼠标位置"的功能,不用在每个组件里重复写
useEffect和事件监听。
🔍 名词 5:副作用(Effect)
-
官方定义:副作用是指在函数执行过程中,对外部环境产生的影响,如 DOM 操作、网络请求、订阅事件等。
-
大白话翻译:组件除了"渲染 UI"之外,做的其他"额外事情"。比如:发请求拿数据、绑定鼠标事件、设置定时器。
-
在代码中如何体现:
javascript
javascriptuseEffect(() => { // 副作用:给 document 绑定了鼠标移动事件 document.addEventListener("mousemove", handleMouseMove); return () => { // 清理副作用:移除事件监听,防止内存泄漏 document.removeEventListener("mousemove", handleMouseMove); }; }, []); // 空依赖数组 = 只在组件挂载时执行一次 -
为什么需要清理:组件卸载后,如果事件监听还在,会导致:
- 内存泄漏:占用的内存无法被回收
- 错误调用:组件都销毁了,事件触发时还在尝试更新已经不存在的组件状态
3. 核心流程图解(把代码跑起来给你看)
流程 1:ThemeContext 的数据流转

流程 2:useMouse 自定义 Hook 的生命周期

4. 重难点深度剖析(核心中的核心)
🔥 难点 1:Provider 的"包裹"机制 ------ 为什么必须包裹?
为什么要有这个设计?
在 React 中,数据流是单向的 (从上到下)。Context 机制打破了"必须逐层传递"的限制,但没有打破"单向流动"的基本原则。
数据必须从上 (Provider)流到下 (Consumer)。这就是为什么 Provider 必须包裹子组件树------只有被包裹的部分才能接收到数据。
如果不用 Provider 会怎样?
javascript
javascript
// 场景:组件没有被 Provider 包裹
function App() {
return (
// 没有 ThemeContext.Provider 包裹
<Page /> // Page 内部调用 useTheme()
);
}
// Page 组件
function Page() {
const theme = useTheme(); // 拿到的 theme 是 createContext 时的默认值 'light'
// 但注意!这个默认值永远不会变化,因为没有任何 Provider 在更新它
}
设计哲学 :React 希望数据流向是可预测的。通过强制 Provider 包裹,你能清晰地知道"数据从哪里来,影响哪些组件"。
代码逐行解剖
javascript
javascript
// ThemeContext.js
import { createContext } from 'react';
// 第 1 行:createContext 创建了一个"容器"
// 参数 'light' 是"默认值"------当没有 Provider 时的后备方案
// 这个容器内部有一个 _currentValue 属性,初始为 'light'
export const ThemeContext = createContext('light');
javascript
javascript
// App.jsx
import { ThemeContext } from './ThemeContext';
function App() {
// 第 2 行:useState 在 React 的 Fiber 节点上创建了一个"记忆单元"
// 这个记忆单元存储了 theme 的当前值 'light' 和更新函数 setTheme
const [theme, setTheme] = useState('light');
return (
// 第 3 行:JSX 语法糖,实际是 React.createElement(ThemeContext.Provider, { value: theme })
// 在 React 内部,Provider 会做两件事:
// 1. 将 value 存入 Fiber 节点的 context 属性中
// 2. 标记这个节点"有新的 context 值需要向下传播"
<ThemeContext.Provider value={theme}>
{/* 第 4 行:Page 和 button 成为 Provider 的 children */}
{/* 在 React 的 commit 阶段,这些 children 会被渲染 */}
{/* 同时,React 会检测到 context 变化,触发所有订阅者的更新 */}
<Page />
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</ThemeContext.Provider>
);
}
javascript
javascript
// useTheme.js
import { useContext } from 'react';
import { ThemeContext } from '../ThemeContext';
export function useTheme() {
// 第 5 行:useContext 是一个"读取器"
// 它会沿着组件树向上查找最近的 ThemeContext.Provider
// 找到后,读取 Provider 的 value 属性
// React 内部:从当前 Fiber 节点的 _contextDependencies 链中查找
// 如果找到了,返回 value;否则返回 createContext 时的默认值
return useContext(ThemeContext);
}
🔥 难点 2:自定义 Hook 的"响应式"封装 ------ 它和普通函数到底有什么区别?
为什么要有这个设计?
React 组件是纯函数 (理想情况下),但现实世界充满了副作用(事件监听、网络请求、定时器)。自定义 Hook 让我们把这些"不纯"的部分封装起来,保持组件的干净。
如果不使用自定义 Hook,你会在每个需要鼠标坐标的组件里写:
javascript
scss
function ComponentA() {
const [x, setX] = useState(null);
const [y, setY] = useState(null);
useEffect(() => {
document.addEventListener('mousemove', (e) => {
setX(e.clientX);
setY(e.clientY);
});
return () => { /* 清理逻辑 */ };
}, []);
// ... 其他逻辑
}
function ComponentB() {
// 同样的代码再写一遍!DRY 原则被破坏了
}
自定义 Hook 的价值:把"状态 + 副作用"打包成可复用的单元。
代码逐行解剖
javascript
javascript
// useMouse.js
import { useState, useEffect } from "react";
export const useMouse = () => {
// ===== 第 1 步:创建响应式状态 =====
// useState 在 Hook 内部创建了两个"记忆单元"
// 注意:每个调用 useMouse 的组件都有自己独立的 x 和 y 状态
// 这就是 Hook 的"状态隔离"特性
const [x, setX] = useState(null);
const [y, setY] = useState(null);
// ===== 第 2 步:定义事件处理函数 =====
// 这个函数在每次鼠标移动时被调用
// 它通过 setX/setY 触发状态更新
function handleMouseMove(e) {
// e.clientX:鼠标在视口中的水平位置(像素)
// e.clientY:鼠标在视口中的垂直位置(像素)
setX(e.clientX); // 触发 React 的调度器,标记这个组件需要更新
setY(e.clientY); // 注意:两次 setState 会被批量合并(在 React 18 的自动批处理中)
}
// ===== 第 3 步:注册副作用 =====
useEffect(() => {
// 副作用:将 handleMouseMove 注册到 document 的 mousemove 事件
// 这里用到了"闭包"------handleMouseMove 捕获了 x、y 的 setter 函数
document.addEventListener("mousemove", handleMouseMove);
// ===== 第 4 步:清理副作用 =====
// 返回的函数称为"清理函数"(cleanup function)
// React 在组件卸载时,以及下次 effect 执行前调用它
return () => {
// 移除事件监听,防止内存泄漏
// 为什么需要手动移除?
// 因为 document 是全局对象,在组件生命周期之外
// 如果不移除,组件销毁后,事件触发时还会调用 handleMouseMove
// 而 handleMouseMove 中的 setX/setY 已经无效(组件已销毁),React 会报错
document.removeEventListener("mousemove", handleMouseMove);
};
}, []); // 空依赖数组 = 只在组件"挂载"时执行一次,卸载时执行清理
// ===== 第 5 步:返回响应式数据 =====
// 返回的对象包含 x 和 y
// 当 x/y 变化时,使用这个 Hook 的组件会自动重新渲染
return { x, y };
};
底层原理:为什么自定义 Hook 能"响应式"?
- Hook 调用链 :每个组件都有一个 Fiber 节点 ,Fiber 上有一个
memoizedState链表,存储所有 Hook 的状态。 - 状态绑定 :当
useState在自定义 Hook 中被调用时,它的状态被绑定到调用该 Hook 的组件的 Fiber 节点上。 - 更新触发 :当
setX被调用时,React 会标记该 Fiber 节点需要更新,然后重新执行组件函数 ,组件函数中调用的useMouse会返回最新的 x。
🔥 难点 3:useEffect 的依赖数组 ------ 为什么空数组只执行一次?为什么需要清理?
为什么要有这个设计?
React 组件的生命周期分为:挂载(Mount)、更新(Update)、卸载(Unmount)。useEffect 让我们在不同的生命周期阶段执行不同的逻辑。
依赖数组让开发者精确控制 effect 的执行时机:
- 不传依赖数组:每次渲染都执行
- 传空数组
[]:只在挂载时执行,卸载时清理 - 传具体依赖
[count]:只在 count 变化时执行
代码逐行解剖
javascript
javascript
useEffect(() => {
// ===== 第 1 阶段:挂载时执行 =====
// 当组件首次渲染到 DOM 后,React 执行这个函数
document.addEventListener("mousemove", handleMouseMove);
// ===== 第 2 阶段:返回清理函数 =====
// 这个函数会在"组件卸载前"执行
// 注意:如果依赖数组不为空,清理函数会在"下次 effect 执行前"执行
return () => {
// 移除事件监听
document.removeEventListener("mousemove", handleMouseMove);
};
}, []); // 空数组:告诉 React "这个 effect 不依赖任何外部变量"
// React 内部:将 effect 标记为"不需要重新执行"
// 在 Fiber 的 updateQueue 中,这个 effect 只在 mount 时执行一次
为什么必须清理?
javascript
scss
// ❌ 错误示范:忘记清理
useEffect(() => {
document.addEventListener("mousemove", handleMouseMove);
// 没有返回清理函数!
}, []);
// 后果:
// 组件 A 挂载 → 添加监听
// 组件 A 卸载 → 监听还在!
// 用户移动鼠标 → 事件触发 → handleMouseMove 执行
// → handleMouseMove 中调用了 setX/setY
// → 但组件 A 已经销毁,没有 Fiber 节点了
// → React 尝试更新不存在的组件 → 报错!
// → 多次切换组件 → 监听越来越多 → 内存泄漏!
javascript
javascript
// ✅ 正确姿势:完整清理
useEffect(() => {
document.addEventListener("mousemove", handleMouseMove);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
};
}, []);
5. 避坑指南:新手的十面埋伏
🚨 坑点 1:Provider 的 value 使用"对象字面量"导致无限重渲染
❌ 错误示范
javascript
javascript
function App() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState({ name: '小明' });
return (
// 每次 App 渲染,都会创建一个新的对象 { theme, setTheme, user, setUser }
// 即使 theme 和 user 都没变,这个对象也是新的
// React 通过 Object.is 比较 value,发现不同 → 所有订阅者强制更新
<ThemeContext.Provider value={{ theme, setTheme, user, setUser }}>
<Page />
</ThemeContext.Provider>
);
}
💥 后果
- 每次父组件渲染,所有用了
useContext(ThemeContext)的组件都会重新渲染 - 即使它们用到的数据根本没变(比如 Page 只用了 theme,但 user 变了导致它也跟着渲染)
- 大规模应用会卡顿、掉帧
✅ 正确姿势
javascript
javascript
function App() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState({ name: '小明' });
// 使用 useMemo 缓存 value,只在依赖变化时重新创建
const contextValue = useMemo(() => ({
theme,
setTheme,
user,
setUser
}), [theme, user]); // 只有 theme 或 user 变化时才创建新对象
return (
<ThemeContext.Provider value={contextValue}>
<Page />
</ThemeContext.Provider>
);
}
🚨 坑点 2:把"所有数据"塞进一个 Context
❌ 错误示范
javascript
ini
// 一个 Context 管所有
const AppContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState(null);
const [cart, setCart] = useState([]);
const [notifications, setNotifications] = useState([]);
// 所有东西混在一起
const value = { theme, setTheme, user, setUser, cart, setCart, notifications, setNotifications };
return (
<AppContext.Provider value={value}>
<Page />
</AppContext.Provider>
);
}
💥 后果
- 修改任何一个状态(比如购物车加了件商品),所有 用了
AppContext的组件都会重新渲染 - 即使这些组件只关心主题,也要被迫更新
- 性能灾难
✅ 正确姿势
javascript
ini
// 按照功能拆分 Context
const ThemeContext = createContext();
const UserContext = createContext();
const CartContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState(null);
const [cart, setCart] = useState([]);
return (
// 层层嵌套,各管各的
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserContext.Provider value={{ user, setUser }}>
<CartContext.Provider value={{ cart, setCart }}>
<Page />
</CartContext.Provider>
</UserContext.Provider>
</ThemeContext.Provider>
);
}
🚨 坑点 3:自定义 Hook 的"条件调用"
❌ 错误示范
javascript
javascript
function Component() {
if (someCondition) {
// ❌ Hook 不能在条件语句中调用!
const { x, y } = useMouse();
return <div>{x}, {y}</div>;
}
return <div>No mouse</div>;
}
💥 后果
React 依赖 Hook 的调用顺序来关联状态。如果条件调用,会导致:
- Hook 调用顺序在不同渲染间不一致
- 状态错乱:
useMouse的 x 可能被当成其他 Hook 的值 - 严重的运行时错误
✅ 正确姿势
javascript
javascript
function Component() {
// ✅ Hook 始终在顶层调用
const { x, y } = useMouse();
if (someCondition) {
return <div>{x}, {y}</div>;
}
return <div>No mouse</div>;
}
黄金法则 :只在 React 函数组件和自定义 Hook 的顶层调用 Hook。
6. 面试官问什么?(备战八股文)
🎯 面试题 1:useContext 和 Redux 有什么区别?什么时候用哪个?
回答大纲:
-
定位不同:
- useContext:React 内置的状态共享方案,解决 props drilling
- Redux:独立的状态管理库,提供完整的 Flux 架构
-
性能差异:
- useContext:只要 Provider 的 value 变化,所有订阅者都会重渲染(需手动优化)
- Redux:通过 connect/useSelector 实现选择性订阅,只有使用的数据变化才更新
-
适用场景:
- 用 useContext:主题切换、用户信息、语言包等低频更新的全局数据
- 用 Redux:复杂表单、购物车、大量交互的高频更新数据,或需要时间旅行调试的场景
-
复杂度:
- useContext:简单,无需额外依赖
- Redux:复杂,需要学习 action、reducer、middleware 等概念
🎯 面试题 2:自定义 Hook 和普通函数的区别是什么?
回答大纲:
-
内部可使用 React Hook:
- 自定义 Hook:可以调用 useState、useEffect、useContext 等
- 普通函数:不能调用 React Hook,否则会报错
-
状态隔离:
- 自定义 Hook:每次调用都有独立的状态副本,互不影响
- 普通函数:没有状态管理能力
-
返回值响应式:
- 自定义 Hook:返回的数据变化会触发组件重新渲染
- 普通函数:只返回计算值,不触发渲染
-
命名规范:
- 必须以
use开头,便于 lint 插件识别和检查
- 必须以
🎯 面试题 3:useEffect 的清理函数会在什么时候执行?
回答大纲:
-
组件卸载时:必然执行,用于清理副作用(事件监听、定时器等)
-
下次 effect 执行前(当依赖数组不为空时):
-
如果依赖变化,React 会先执行上次 effect 的清理函数,再执行新的 effect
-
例如:
useEffect(() => { ... }, [count]),当 count 从 1 变到 2:- 执行上次的清理函数(count=1 时)
- 执行新的 effect(count=2 时)
-
-
React 渲染阶段 :清理函数的执行在布局和绘制之后,不会阻塞浏览器渲染
-
为什么需要清理:
- 防止内存泄漏(事件监听、定时器未释放)
- 防止状态更新在已卸载的组件上执行(React 警告)
总结:你现在的知识版图
通过这份源码拆解,你掌握了:
| 概念 | 理解程度 |
|---|---|
| Context 机制 | 理解数据如何跨层级传递,Provider 为什么必须包裹 |
| useContext | 知道如何"消费"数据,理解其响应式原理 |
| 自定义 Hook | 理解如何封装"状态+副作用",知道与普通函数的区别 |
| useEffect | 理解依赖数组的作用,知道为什么需要清理 |
| 性能优化 | 理解 value 缓存、Context 拆分的重要性 |
| 避坑指南 | 掌握三个常见错误的解法 |
下一步建议:
- 打开你的项目,找出所有 props 传递超过 3 层的地方,尝试用 Context 重构
- 写一个自定义 Hook,封装 window 的 resize 事件
- 在 React DevTools 的 Profiler 中观察优化前后的渲染性能