Hooks 原理(useState /useEffect)
前言:现如今,使用 React Hooks 函数式编程这种开发方式,早已成为 React 开发主流,相比类组件它更加灵活,扩展性更高的同时还解决了类组件的痛点。本文将核心围绕 Hooks 设计初衷、原理、模拟代码实现进行解析,帮助大家理解与学习 Hooks。
一、Hooks 设计初衷
- 解决类组件的痛点
- 状态逻辑复用困难:类组件的状态逻辑耦合在组件内部,高阶组件(HOC)、Render Props 等复用方式会导致组件嵌套过深("嵌套地狱")。
- 组件复杂度高:生命周期函数(如 componentDidMount/componentDidUpdate)会混杂不同逻辑(数据请求、事件监听、定时器),代码可读性差。
- 类组件学习成本高:this 指向、绑定事件、继承等概念增加新手学习成本。
- Hooks 核心目标
- 让函数组件拥有状态管理和生命周期能力。
- 将组件中分散的逻辑拆分为独立的 Hooks,实现逻辑复用。
- 简化代码结构,降低组件复杂度,让代码更易维护。
二、Hooks 规则与底层原因
- 核心规则
- 只能在函数组件 / 自定义 Hooks 中调用 Hooks。
- 只能在函数顶层调用 Hooks,不能在循环、条件、嵌套函数中调用。
- 规则底层原因
- React 内部通过数组 + 索引存储 Hooks 状态,组件每次渲染时按调用顺序匹配状态。
- 若在条件 / 循环中调用 Hooks,会导致每次渲染时 Hooks 调用顺序不一致,索引匹配错误,状态错乱。
- 若在非函数组件中调用,React 无法关联组件实例,无法维护对应的 Hooks 状态容器。
三、useState 核心原理
- 核心逻辑
- 用数组存储每个 useState 的状态,用索引标记当前 Hooks 位置。
- 每次组件渲染时重置索引,按顺序读取 / 更新状态。
- setState 触发状态更新后,重新执行组件函数,更新视图。
- 简化版 useState
js
// 模拟React内部存储 Hooks 的容器
let hooks = [];
// 记录当前执行到第几个 Hook
let currentHookIndex = 0;
function useState(initialState){
const currentIndex = currentHookIndex
hooks[currentIndex] = hooks[currentIndex] || initialState
console.log('currentIndex',hooks[currentIndex],currentIndex)
const setState = (newValue) => {
if(typeof newValue === 'function'){
hooks[currentIndex] = newValue(hooks[currentIndex])
}else {
hooks[currentIndex] = newValue
}
renderComponent()
}
currentHookIndex++
return [hooks[currentIndex], setState]
}
// 每次渲染前重置索引
function renderComponent(component) {
// 每次渲染组件前,重置 Hook 索引为 0
currentHookIndex = 0;
// 模拟组件rerender,在39行打印能取到更新后的值。实际上react的更新还包括了批量更新操作
// TestComponent();
}
function TestComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('initValue');
// 模拟点击事件更新状态
setTimeout(() => {
setCount(10)
setName('test-setState')
console.log('update---------', count, name)
}, 1000);
}
// 执行组件(模拟首次渲染)
TestComponent();
- 核心要点
- hooks 数组:存储所有 useState 的状态,索引对应 Hooks 调用顺序。
- currentHookIndex:标记当前执行到第几个 Hook,保证状态与 Hook 一一对应。
- setState:修改 hooks 数组对应索引的值,并触发组件重新渲染(重置索引后执行组件)。
四、useEffect 核心原理
- 核心逻辑
模拟生命周期:useEffect 分为 "执行回调" 和 "清理副作用" 两个阶段。
依赖数组:通过对比依赖数组的前后值,判断是否执行回调。
执行时机:组件渲染完成后执行(模拟浏览器 setTimeout 实现)。 - 简化版 useEffect
js
// 扩展 Hooks 容器:存储 useEffect 的依赖和回调
let hooks = [];
let currentHookIndex = 0;
// 存储 useEffect 的依赖(用于对比)
let effectDeps = [];
/**
* 简化版 useEffect
* @param {Function} callback 副作用回调
* @param {Array} deps 依赖数组
*/
function useEffect(callback, deps) {
const currentIndex = currentHookIndex;
// 获取上一次的依赖
const prevDeps = effectDeps[currentIndex];
// 判断是否需要执行回调:依赖不存在 / 依赖有变化
const hasChanged = !prevDeps || !deps.every((dep, i) => dep === prevDeps[i]);
if (hasChanged) {
// 执行清理函数(上一次的回调返回值)
const cleanUp = hooks[currentIndex];
if (cleanUp && typeof cleanUp === 'function') {
cleanUp();
}
// 模拟 useEffect 异步执行(浏览器渲染完成后)
setTimeout(() => {
// 保存清理函数到 hooks 数组
hooks[currentIndex] = callback();
});
// 更新当前依赖
effectDeps[currentIndex] = deps;
}
currentHookIndex++;
}
// 复用之前的 renderComponent 函数
function renderComponent(component) {
currentHookIndex = 0;
// component();
}
// 测试用例
function TestComponent() {
const [count, setCount] = useState(0);
// 模拟监听副作用
useEffect(() => {
console.log('副作用执行:count =', count);
// 清理函数
return () => {
console.log('清理副作用:count =', count);
};
}, [count]); // 依赖 count,仅当 count 变化时执行
// 模拟更新
setTimeout(() => {
setCount(1);
}, 1000);
}
// 首次渲染
renderComponent(TestComponent);
- 依赖数组分析
| 依赖数组 | 执行时机 | 适用场景 |
|---|---|---|
| 无依赖数组 | 每次组件渲染后执行 | 监听所有状态变化,如全局事件监听 |
| 空数组 [] | 仅组件首次渲染后执行(模拟 componentDidMount) | 只执行一次的逻辑,如数据请求、定时器创建 |
| 有依赖 [a, b] | 仅当 a/b 变化时执行 | 关联特定状态的副作用,如根据 ID 请求数据 |
五、核心原理总结
- useState 核心
- 基于数组 + 索引存储状态,依赖调用顺序匹配状态。
- setState 本质是修改状态容器的值,并触发组件重新渲染。
- 每次渲染重置索引,保证 Hooks 顺序一致。
- useEffect 核心
- 基于依赖对比决定是否执行副作用回调。
- 支持清理函数,解决副作用泄漏问题(如事件解绑、定时器清除)。
- 异步执行回调,避免阻塞浏览器渲染。
- Hooks 规则底层逻辑
- 禁止条件 / 循环中调用:保证 Hooks 调用顺序不变,索引匹配正确。
- 仅函数组件 / 自定义 Hooks 中调用:保证状态能关联到对应的组件实例。