React Hooks 源码级揭秘:为什么必须按顺序调用?

在使用 React Hooks 时,你是否被 ESLint 警告过:"React Hook must be called in the exact same order in every component render"?

为什么不能在 if 语句或循环中调用 Hooks?今天,我们从 单向链表索引偏移 的底层视角,深度剖析 React Hooks 的实现机制。

1. Hooks 的本质:链表上的节点

在 React Fiber 架构中,每个函数组件实例都对应一个 Fiber 节点。而该组件内调用的所有 Hooks,则通过一个单向链表串联起来。

1.1 Hook 数据结构

arduino 复制代码
class Hook {
    memoizedState: any;  // 保存的状态值
    baseState: any;      // 基础状态
    queue: UpdateQueue;  // 更新队列
    next: Hook | null;   // 指向下一个 Hook
}

1.2 链表构建过程

假设组件代码如下:

scss 复制代码
function App() {
    const [a, setA] = useState(0); // Hook 1
    const [b, setB] = useState(1); // Hook 2
    useEffect(() => {}, []);       // Hook 3
}

内存中的链表结构如下:

php 复制代码
Fiber.memoizedState 
    → Hook1 (useState: a) 
    → Hook2 (useState: b) 
    → Hook3 (useEffect) 
    → null

2. 为什么必须按顺序调用?

2.1 索引偏移灾难

React 并不通过"名字"来识别 Hook,而是通过调用顺序(Index)

场景演示:

scss 复制代码
// 第一次渲染
function App() {
    const [name, setName] = useState('Lee'); // Index 0
    if (someCondition) {
        const [age, setAge] = useState(18);  // Index 1
    }
}

如果 someConditiontrue 变为 false

  1. React 依然会从链表的 Index 1 处取值。
  2. 但此时 Index 1 对应的可能是另一个完全不同的 Hook(或者 null)。
  3. 导致状态错位,甚至程序崩溃。

2.2 核心算法:指针移动

ini 复制代码
function useState(initialState) {
    // 1. 确定当前操作的 Hook
    let hook;
    if (isMount) {
        hook = createNewHook(initialState);
        appendToLinkedList(hook);
    } else {
        hook = currentHook;
        currentHook = currentHook.next; // 关键:指针后移
    }
    
    return [hook.memoizedState, dispatchAction];
}

关键点 :每次调用 useState,内部的 workInProgressHook 指针都会向后移动一位。如果调用顺序变了,指针就会指错位置。

3. 状态更新机制:环形链表与批处理

3.1 更新队列(Update Queue)

当你调用 setState 时,React 并不会立即修改状态,而是创建一个 Update 对象并加入队列:

ini 复制代码
const update = {
    action: action, // 传入的值或函数
    next: null
};

// 将 update 加入环形链表
if (queue.pending === null) {
    update.next = update; // 自己指向自己
} else {
    update.next = queue.pending.next;
    queue.pending.next = update;
}
queue.pending = update;

3.2 批量计算新状态

在下一次渲染时,React 会遍历这个环形链表,依次执行所有的更新:

ini 复制代码
let newState = hook.baseState;
let update = firstUpdate;
do {
    const action = update.action;
    newState = typeof action === 'function' ? action(newState) : action;
    update = update.next;
} while (update !== firstUpdate);

hook.memoizedState = newState;

4. useEffect 的依赖对比

useEffect 的核心在于浅比较(Shallow Compare)

ini 复制代码
function useEffect(create, deps) {
    const hook = getWorkInProgressHook();
    
    if (deps) {
        const prevDeps = hook.memoizedState?.deps;
        // 逐项对比
        if (prevDeps && deps.every((d, i) => Object.is(d, prevDeps[i]))) {
            return; // 依赖没变,跳过执行
        }
    }
    
    // 执行副作用
    hook.memoizedState = { create, deps };
    scheduleEffect(create);
}

5. 工业界实战:自定义 Hooks 封装

5.1 useLocalStorage

javascript 复制代码
function useLocalStorage(key, initialValue) {
    const [state, setState] = useState(() => {
        const stored = localStorage.getItem(key);
        return stored ? JSON.parse(stored) : initialValue;
    });

    useEffect(() => {
        localStorage.setItem(key, JSON.stringify(state));
    }, [key, state]);

    return [state, setState];
}

5.2 useAsync(异步状态管理)

php 复制代码
function useAsync(asyncFunction) {
    const [state, setState] = useState({ loading: false, data: null, error: null });

    const execute = useCallback(() => {
        setState({ loading: true, data: null, error: null });
        asyncFunction()
            .then(data => setState({ loading: false, data, error: null }))
            .catch(error => setState({ loading: false, data: null, error }));
    }, [asyncFunction]);

    return { ...state, execute };
}

6. 面试考点

Q1: 为什么 Hooks 不能放在条件语句中?

A: React 依靠 Hooks 的调用顺序(Index)来匹配链表中的节点。如果顺序改变,会导致状态读取错位,引发严重的 Bug。

Q2: useState 的更新是同步还是异步的?

A: 在 React 18 之前,合成事件中的更新是异步批处理的。在 React 18 中,引入了自动批处理(Automatic Batching),即使是 Promise 或 setTimeout 中的更新也会合并渲染。

Q3: useRef 和 useState 有什么区别?

A: useState 更新会触发组件重新渲染,而 useRef 只是修改一个普通对象的 .current 属性,不会触发渲染。useRef 常用于保存不需要反映在 UI 上的可变值。

7. 总结

React Hooks 的设计美学:

  1. 极简的 API:用函数式写法实现了类组件的所有功能
  2. 高效的链表:O(1) 复杂度的状态存取
  3. 严格的约束:通过顺序约束换取了极致的性能和简洁性

理解 Hooks 的链表本质,你就不再是"背规则",而是真正掌握了 React 的心跳。


如果你觉得这篇关于"React 底层原理"的文章对你有帮助,欢迎点赞收藏!🚀

相关推荐
之歆2 小时前
DAY_20JavaScript 条件语句与循环结构深度学习(二)
前端·javascript
漓漾li2 小时前
每日面试题-前端
前端·react.js·面试
布局呆星2 小时前
Vue3 路由守卫详解:全局守卫、路由独享守卫、组件内守卫
前端·javascript·vue.js
小李子呢02112 小时前
前端八股Vue---ref操作 DOM 元素或组件,调用子组件方法
前端·javascript·vue.js
yqcoder2 小时前
深入理解 JavaScript:什么是可迭代对象 (Iterable)?
开发语言·javascript·网络
kyriewen113 小时前
我开发的 Chrome 扒图浏览器插件又更新了❗
前端·javascript·chrome·科技·ai
晓得迷路了3 小时前
栗子前端技术周刊第 128 期 - Rolldown 1.0、Vitest、Node.js 26.0.0...
前端·javascript·css
qingy_20463 小时前
浏览器页面出现竖向滚动条的解决方案
前端·javascript·vue.js
之歆3 小时前
DAY_17深度博客:CSS 响应式布局 · BFC · JavaScript 完全指南(下)
前端·javascript·css