Day 3:Hooks 原理

📅 Day 3:Hooks 原理

学习目标:彻底搞懂 React 是怎么记住你的 state 的


👴 老大爷能听懂版

Hooks 是啥?

想象你在流水线工厂上班:

流水线上的东西 对应 Hooks
第一个工位的冰箱 useState(存状态)
第二个工位的定时器 useEffect(定时干活)
第三个工位的对讲机 useContext(跨部门喊话)
每个工位有个编号 Hooks 调用顺序(不能乱!)

简单说:Hooks 就是给函数组件提供"状态"和"生命周期"的工具。


React 怎么记住你的 state?

想象一个火车站的寄存柜:

javascript 复制代码
// 你写的代码
function App() {
  const [count, setCount] = useState(0);
  // ...
}

React 背后干的活:

yaml 复制代码
┌─────────────────────────────────────────────┐
│            组件 (App) 的"行李"              │
├─────────────────────────────────────────────┤
│  Hook 1: { state: 0, next: Hook2 }         │  ← useState(0)
│  Hook 2: { effect: ..., next: null }       │  ← useEffect(...)
│  Hook 3: { context: ..., next: null }      │  ← useContext(...)
└─────────────────────────────────────────────┘
                        ↓
                链表串起来!

关键点:Hooks 是用"链表"存储的!


为什么 Hooks 必须按顺序调用?

想象点菜:

javascript 复制代码
// ✅ 正确:每次都按顺序点
function App() {
  const [a, setA] = useState(1);   // 第一个菜:宫保鸡丁
  const [b, setB] = useState(2);   // 第二个菜:麻婆豆腐
  const [c, setC] = useState(3);   // 第三个菜:鱼香肉丝
}

// ❌ 错误:顺序乱了
function App() {
  const [a, setA] = useState(1);
  if (show) {
    const [b, setB] = useState(2);  // 这菜不一定点!
  }
  const [c, setC] = useState(3);   // 顺序全乱了!
}

原因:React 用"序号"找 Hook,就像餐厅按顺序上菜,顺序乱了就读错数据了!


useState 怎么工作的?

场景:买奶茶

步骤 实际发生的事
init:你要一杯奶茶 useState(0) 创建初始 state
存:服务员记录下来 React 把它存到 Hook 对象的 memoizedState
改:你说"换成半糖" 调用 setCount(1),触发重新渲染
取:再要一杯 React 再次运行组件,从 Hook 里取出 memoizedState

useEffect 怎么工作的?

场景:设闹钟

javascript 复制代码
useEffect(() => {
  console.log('组件挂载了!');
  return () => console.log('清理!');
}, [依赖]);
阶段 比喻 实际发生
组件挂载 闹钟响了 执行回调函数
依赖变化 重新设闹钟 先执行清理函数,再执行新回调
组件卸载 闹钟关了 执行清理函数

注意:useEffect 是在渲染完成后执行的,不是渲染期间!


useRef 和 useState 的区别?

场景:存钱罐 vs 记事本

useState useRef
比喻 存钱罐(变了会引起注意) 记事本(变了不声张)
修改后 触发重新渲染 不触发重新渲染
用途 UI 需要跟着变的值 存 DOM 引用、计时器等
javascript 复制代码
// useState:改了会重新渲染
const [count, setCount] = useState(0);
setCount(1);  // 组件重新渲染!

// useRef:改了不重新���染
const countRef = useRef(0);
countRef.current = 1;  // 不会重新渲染!

💻 专业开发者版

Hooks 的核心数据结构:链表

javascript 复制代码
// packages/react-reconciler/src/ReactFiberHooks.js

// 每个 Hook 就是一个链��节点
const hook = {
  memoizedState: null,  // 存储的状态(useState 的值、useEffect 的回调等)
  baseState: null,      // 基础状态
  baseQueue: null,      // 基础队列
  queue: null,          // 更新队列
  next: null,           // 指向下一个 Hook
};

链表结构:

yaml 复制代码
Fiber.memoizedState (第一个 Hook)
       ↓
   Hook1 { state: 0, next: Hook2 }
       ↓
   Hook2 { effect: fn, next: Hook3 }
       ↓
   Hook3 { context: ..., next: null }

React 包中的 Hooks 入口

javascript 复制代码
// packages/react/src/ReactHooks.js

// 所有 Hook 都长这样:找 dispatcher,然后把调用传给它
export function useState<S>(initialState) {
  const dispatcher = resolveDispatcher();  // ← 找到实际的实现
  return dispatcher.useState(initialState); // ← 调用它
}

function resolveDispatcher() {
  // ReactSharedInternals.H 就是那个神秘的 H( dispatcher)
  const dispatcher = ReactSharedInternals.H;
  return dispatcher;
}

useState 源码解析

javascript 复制代码
// packages/react-reconciler/src/ReactFiberHooks.js

// 1. 创建 Hook(链表节点)
function mountStateImpl(initialState) {
  const hook = mountWorkInProgressHook();  // 创建新 Hook
  
  // 存到 hook.memoizedState
  hook.memoizedState = hook.baseState = initialState;
  
  // 创建更新队列
  const queue = {
    pending: null,      // 待处理的更新
    lanes: NoLanes,
    dispatch: null,     // setXxx 函数
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;
  
  return hook;
}

// 2. 真正创建 useState
function mountState(initialState) {
  const hook = mountStateImpl(initialState);
  
  // 创建 dispatch 函数(就是 setXxx)
  const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
  
  return [hook.memoizedState, dispatch];  // 返回 [值, set函数]
}

useEffect 源码解析

javascript 复制代码
// useEffect 的数据结构
const fiberEffect = {
  tag: HookLayout | HookPassive,  // 标记类型
  create: () => () => void,       // 回调函数
  destroy: () => void,            // 清理函数
  deps: Array,                    // 依赖数组
  next: null,                     // 下一个 effect
};

执行时机:

  1. 渲染阶段:不做任何事情
  2. 渲染完成后(Commit 阶段):调度 effect
  3. 下一次绘制前:执行上次 cleanup + 新的 effect

面试必问题:为什么不能条件调用 Hooks?

javascript 复制代码
// ❌ 错误示范
function App() {
  const [a, setA] = useState(1);
  
  if (condition) {
    const [b, setB] = useState(2);  // 可能不执行!
  }
  
  const [c, setC] = useState(3);  // 这时候 c 可能错位到 b 的位置
}

// ✅ 正确示范
function App() {
  const [a, setA] = useState(1);
  const [b, setB] = useState(2);  // 始终调用
  const [c, setC] = useState(3);  // 始终调用
  
  // 用 if 控制业务逻辑,不要控制 Hooks 调用
  if (condition) {
    // 在这里用 b 的值
  }
}

原因:Hooks 是按"调用顺序"存储在链表中的,第一次渲染时Hooks 是 [a, b, c],第二次如果 b 没调用,React 就会把 c 当成 b,导致数据错乱!


Hooks 调用规则源码验证

javascript 复制代码
// packages/react/src/ReactHooks.js

function resolveDispatcher() {
  const dispatcher = ReactSharedInternals.H;
  
  // 如果 dispatcher 是 null,说明没在组件里调用
  if (dispatcher === null) {
    console.error(
      'Invalid hook call. Hooks can only be called inside of ' +
      'the body of a function component.'
    );
  }
  return dispatcher;
}

📋 面试必考点

Q1:useState 和 useRef 的区别?

答:

useState useRef
修改后触发重渲染 ✅ 会 ❌ 不会
存储值的位置 memoizedState memoizedState.current
典型用途 UI 状态 DOM 引用、计时器、不触发渲染的值

一句话: useState 变了会让组件"重新出生",useRef 变了假装没看见。

Q2:为什么 Hooks 不能在条件/循环/嵌套函数中调用?

答:

  • Hooks 依赖"调用顺序"来匹配对应的 state
  • 顺序记录在链表里:hook1.memoizedStatehook2.memoizedState
  • 如果顺序变了,React 就找不到对应的 state 了
  • 这也是为什么 React 报错 "Rendered more hooks than during the previous render"

Q3:useEffect 的执行时机?

答:

  • 渲染阶段:不执行
  • 渲染完成后(Commit 阶段):调度 effect
  • 下一个 paint 之前:执行上次 cleanup + 执行新 effect
  • DOM 已经更新,所以可以安全操作 DOM

Q4:useLayoutEffect 和 useEffect 的区别?

答:

useEffect useLayoutEffect
执行时机 渲染完成后,paint 之后 渲染完成后,paint 之前
阻塞 DOM 更新? 不阻塞 阻塞
适用场景 数据获取、订阅 测量 DOM、修改 DOM

简单说: 需要"同步"操作 DOM 用 useLayoutEffect,其他用 useEffect。

Q5:useMemo 和 useCallback 的区别?

答: 本质都是"缓存"

  • useMemo(computeFn, deps) --- 缓存计算结果
  • useCallback(fn, deps) --- 缓存函数本身 (等于 useMemo(() => fn, deps)

📝 今日总结

Hook 老大爷版 程序员版
useState 冰箱(存食材) 存状态,触发重渲染
useEffect 定时器闹钟 副作用,渲染后执行
useRef 记事本(不声张) 存值不触发渲染
useContext 传菜窗口 跨组件共享数据
useMemo 备忘录 缓存计算结果
useCallback 记事本(函数版) 缓存函数

🎯 今日自测

  1. Hooks 为什么必须按顺序调用?
  2. useState 返回的 setXxx 做了什么?
  3. useEffect 的清理函数什么时候执行?
  4. useRef 修改后为什么不会触发重渲染?
  5. useMemo 和 useCallback 的区别是?

📅 明日预告

Day 4:Fiber 架构

本日概念 明日进阶
链表存储 Fiber 树结构
渲染后更新 render vs commit 阶段
Hook 顺序 异步调度

下节预告:React 是怎么"画"页面的?Fiber 架构大揭秘!🚀

相关推荐
我命由我123452 小时前
JS 开发问题:url.includes is not a function
开发语言·前端·javascript·html·ecmascript·html5·js
weixin199701080162 小时前
义乌购商品详情页前端性能优化实战
前端·性能优化
汪啊汪2 小时前
Day 2:JSX 转换原理
前端
学以智用2 小时前
Vue3 + Vue Router 4 完整示例(可直接运行)
前端·vue.js
程序员小李白2 小时前
vue2基本语法详细解析(2.7条件渲染)
开发语言·前端·javascript
SuperEugene2 小时前
Vue3 项目目录结构规范:按业务域划分,新人快速上手|项目规范篇
前端·javascript·vue.js
悟空瞎说2 小时前
# 10年前端血坑:Canvas drawImage画不出图?90%的人栽在这几步
前端
qibmz2 小时前
新电脑安装 nvm 卡住?无需修改配置文件,一行命令完美解决!
前端
遗憾随她而去.2 小时前
高德地图自定义点标记: SVG vs HTML+CSS两种方案
前端·css