前端工程Technical Deep Dive
React Hooks 源码解析 —— useState、useEffect 原理深度剖析
发布时间2026/03/29
分类前端工程
预计阅读2 分钟
作者吴长龙
*
为什么需要理解 Hooks 源码
01.内容
为什么需要理解 Hooks 源码
Hooks 2019 年引入 React,彻底改变了组件写法。但很多人只是"会用",不清楚为什么有时候会出现令人困惑的行为。本文将从源码角度解析 Hooks 的工作原理。
Hooks 的基本结构
React 内部用链表存储 Hook 信息。每个组件对应一个 Hooks 链表:
javascript snippetjavascript
// 简化版 Hook 结构
{
memoizedState: 缓存的状态值,
baseState: 基础状态,
queue: 待执行的更新队列,
next: 下一个 Hook,
// ...
}关键点:Hooks 按调用顺序存储,这就是为什么 Hooks 不能在条件语句中调用。
useState 原理
#### 基本实现
javascript snippetjavascript
function useState(initialState) {
let hook = currentlyRenderingComponent.hooks[index];
if (!hook) {
// 首次渲染:创建 Hook
hook = {
memoizedState: initialState,
queue: { pending: null },
next: null,
};
if (!firstHook) {
firstHook = hook;
} else {
hooksTail.next = hook;
}
hooksTail = hook;
}
// 更新逻辑
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let update = hook.queue.pending;
baseState = typeof update.action === 'function'
? update.action(baseState)
: update.action;
hook.queue.pending = null;
hook.memoizedState = baseState;
}
return [baseState, dispatchAction.bind(null, hook.queue)];
}#### 为什么 useState 调用顺序很重要
jsx snippetjsx
// 错误示例
function MyComponent() {
const [count, setCount] = useState(0);
if (count > 0) {
const [name, setName] = useState(''); // 条件调用 Hook!
}
return <div>{name}</div>;
}如果 count 为 0,组件渲染时只调用了 useState(0)。但当 count 变为 1 时,会调用两次 useState,React 就会混乱——它会尝试用第二个 Hook 的数据结构去读取 name,导致 bug。
这就是 Hook 调用规则的底层原因。
useEffect 原理
#### 差异于 useLayoutEffect
javascript snippetjavascript
function useEffect(create, deps) {
// 标记为需要异步调度的副作用
currentFiber.flags |= UpdateEffect | PassiveEffect;
hook.memoizedState = pushEffect(
HookHasEffect,
create,
deps
);
}
function useLayoutEffect(create, deps) {
// 标记为需要同步调度的副作用
currentFiber.flags |= UpdateEffect;
hook.memoizedState = pushEffect(
HookHasEffect,
create,
deps
);
}关键区别在于 flags 不同:
- •
useEffect:额外添加PassiveEffect,告诉 React 在渲染完成后异步执行 - •
useLayoutEffect:同步执行,在 DOM 更新前运行
#### 依赖项比较
javascript snippetjavascript
function areHookInputsEqual(nextDeps, prevDeps) {
if (!prevDeps) return false;
for (let i = 0; i < prevDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}React 使用 Object.is 进行依赖比较(类似 ===,但 NaN === NaN 为 true,+0 !== -0)。
useMemo 与 useCallback
javascript snippetjavascript
function useMemo(nextCreate, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function useCallback(callback, deps) {
return useMemo(() => callback, deps);
}重要:useMemo 的 nextCreate 是函数返回值,而不是函数本身。如果你写 useMemo(() => fn, deps),每次渲染都会执行 fn(),但结果可能被缓存。
闭包陷阱
jsx snippetjsx
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log('Count:', count); // 这里的 count 永远是 0!
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖,只运行一次
return <div onClick={() => setCount(c => c + 1)}>{count}</div>;
}问题:Effect 只在首次渲染后执行,此时 count 是 0。定时器闭包捕获了 count = 0,之后永远读不到新值。
解决方案:
jsx snippetjsx
// 方案1:使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // 使用最新状态
}, 1000);
return () => clearInterval(timer);
}, []);
// 方案2:加入依赖
useEffect(() => {
const timer = setInterval(() => {
console.log('Count:', count);
}, 1000);
return () => clearInterval(timer);
}, [count]); // count 变化时重新创建定时器自定义 Hook
自定义 Hook 不过是复用状态逻辑的函数:
javascript snippetjavascript
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
return [value, toggle];
}
// 使用
function App() {
const [isOn, toggle] = useToggle();
return <button onClick={toggle}>{isOn ? 'ON' : 'OFF'}</button>;
}自定义 Hook 的核心是组合多个基础 Hook,创造新的抽象。
性能优化建议
- •useMemo 避免不必要的计算
jsx snippetjsx
const expensive = useMemo(() => compute(a, b), [a, b]);- •useCallback 稳定回调函数
jsx snippetjsx
const handleClick = useCallback(() => {
doSomething(count);
}, [count]);- •useRef 存储可变值,不触发重渲染
jsx snippetjsx
const ref = useRef(null);
ref.current = '变化但不触发重渲染';- •合理设置依赖项
依赖项过多会导致频繁重建,依赖项过少会导致闭包陷阱。
总结
理解 Hooks 源码能帮助我们:
- •避免常见 bug(如条件调用 Hook、闭包陷阱)
- •更合理地选择 Hook 方案
- •在性能优化时有的放矢
- •更好地理解 React 的设计哲学