深入浅出 React Hooks 原理:从 Fiber 的 memoizedState 链表讲到 updateQueue 调度

这篇文章想解决 3 个常见问题:

  • 为什么 Hook 不能写在 if 里?
  • 为什么依赖数组漏了一个值就会出现"旧状态/旧闭包"?
  • 为什么 setState 之后立刻拿到的还是旧值?

下面我们换个更直观的角度:

  • Hooks 的数据到底"放哪儿了"?
  • setState 之后为什么不会立刻变?
  • 依赖数组漏写为什么会拿到"旧闭包"?

把这三点串起来,你就能把上面 3 个问题一次性想明白。

1. "副作用"到底是什么?(为什么需要 useEffect)

React 是基于 UI 驱动的前端框架,UI 是 props/state 的函数结果。理想情况下,渲染应该是纯函数:输入确定,输出也确定,不依赖和修改任何外部变量,这叫"没有副作用"。

但实际开发中经常要做这些事情:数据请求、定时器、DOM 操作等。这些都依赖或改变"组件外部"的世界,所以被称为副作用。

2. Hooks 的引出:函数组件怎么拥有状态与副作用能力

为了让函数组件也能拥有"状态"和"副作用处理能力",React 引入 Hooks。

比如 useStateuseEffect 等 API,让函数组件也能维护内部状态,并在合适的时机处理请求、订阅、DOM 操作等副作用。

3. Hook 的数据存在哪?

3.1 Fiber 节点:函数组件的"档案袋"

在 React 内部,你可以把 Fiber 节点理解成"这个组件实例的身份证":组件每在页面里出现一次,就会有一个对应的 Fiber。

组件所有的 Hooks 数据(state、ref、effect 的一些信息等),都会跟着这个 Fiber 走,最关键的入口就是 memoizedState

另外,Fiber 也是 React 用来做调度和更新的基本单位。跟我们这篇文章关系最大的几个字段就是:memoizedPropsmemoizedStateupdateQueue

顺带说一句虚拟 DOM:它本质就是用普通 JS 对象描述"界面长什么样",React 会在合适的时机把这些变化真正同步到浏览器的 DOM 上。

比如下面这一段代码,展示 DOM 和虚拟 DOM 之间的对应关系:

jsx 复制代码
<div id="app" class="container">
  <p>hello</p>
</div>
jsx 复制代码
{
  type: 'div',
  props: {
    id: 'app',
    className: 'container'
  },
  children: [
    { type: 'p', children: 'hello' }
  ]
}

3.2 memoizedState 是什么?它为什么是"链表"

memoizedState 本身是一个链表头 。链表中按固定顺序 存储所有 Hook 的状态:包括 useState 的值、useReducer 的 state、useRefuseEffect 的依赖等。

例如:

jsx 复制代码
function Demo() {
  const [count, setCount] = useState(0)
  const [text, setText] = useState('abc')
  return <div>{count} {text}</div>
}

React 内部会为每个 Hook 创建一个 hook 对象

jsx 复制代码
const hook1 = {
  memoizedState: 0,  // count 的值
  next: hook2       // 指向第二个 hook
}

const hook2 = {
  memoizedState: 'abc',// 第二个 useState 的缓存值
  next: null           // 链表尾部
}

然后把第一个 hook 挂载到 Fiber 节点:

jsx 复制代码
fiberNode.memoizedState = hook1

最终形成:

jsx 复制代码
fiberNode.memoizedState = {
  memoizedState: 0,
  next: {
    memoizedState: 'abc',
    next: null
  }
}

所以:fiberNode.memoizedState 就是 Hook 链表的头结点

3.3 为什么 Hooks 不能在 if / for / 嵌套函数里使用?

因为 React 是按顺序 ,从 memoizedState 链表从头往后依次取 Hook 的:

  • 第 1 次调用 useState → 取链表第 1 个 hook
  • 第 2 次调用 useState → 取链表第 2 个 hook
  • 第 3 次调用 useEffect → 取链表第 3 个 hook

如果在 if 里写 Hook:

jsx 复制代码
if (someCondition) {
  const [count, setCount] = useState(0)
}
const [text, setText] = useState('abc')
  • 第一次渲染:条件成立 → 链表顺序是 useState → useState
  • 第二次渲染:条件不成立 → 少执行一个 Hook,后面的 Hook 取到的"位置"就错了

**链表顺序对不上,状态就会串位。**所以 React 强制要求:所有 Hooks 必须在组件顶层,并按固定顺序执行。

4. updateQueue:更新任务先存哪?

为什么 setState 之后立刻读到的还是旧值呢?

核心点是:setState 不是"立即改 memoizedState",而是先把更新放进队列,等 React 调度到这一轮渲染时再统一计算。

4.1 updateQueue 介绍:

组件最终生效的状态会存在 memoizedState 里。

那在"状态更新还没真正计算、还没生效之前",这些更新任务存在哪?答案就是:updateQueue

它的作用是:收集所有尚未处理的 state 更新,形成一个更新队列

例如:

jsx 复制代码
const [count, setCount] = useState(0)

setCount(1)
setCount(2)
setCount(3)

连续调用三次 setCount 并不会立即修改 memoizedState

React 会先创建 3 个更新对象,放进 updateQueue 排队:

jsx 复制代码
fiber.updateQueue = {
  pending: {
    action: 1,
    next: {
      action: 2,
      next: {
        action: 3,
        next: ... // 环形链表,最后指回第一个
      }
    }
  }
}

4.2 updateQueue 如何把更新跑起来:

updateQueue 自己不会"变更界面",它更像一个盒子:先把这次要改什么记下来。

当把更新对象加入 updateQueue 时:

  1. React 会把这个 fiber 标记为"有更新"。
  2. 根据更新优先级发起调度。
  3. 调度开始后进入 render 阶段:遍历 Fiber 树,从 updateQueue 取出更新,计算出最终新 state,保存到 memoizedState。
  4. React 会对比前后两次"界面应该长什么样",算出需要改动的那一小部分。
  5. commit 阶段修改 DOM,最终更新视图。

4.3 为什么 setState 后立刻打印还是旧值?

因为你"调用 setState 的那一刻",更新还只是进入了 updateQueue。

真正写入 memoizedState 并触发重新渲染,要等 React 在后续调度中跑完 render/commit。

另外,如果你的新状态依赖旧状态,建议用函数式更新,避免读到旧值:

jsx 复制代码
setCount(c => c + 1)

5. 为什么依赖数组漏值会出现"旧状态/旧闭包"(解决:依赖数组问题)

关键点是:每次渲染都会产生一个新的闭包

useEffect 如果依赖数组是空的,它只会在首次渲染时记录那次渲染产生的回调函数,后面的渲染不会替换掉它,于是回调里拿到的 state/props 就会"停留在第一次"。

例如:

jsx 复制代码
useEffect(() => {
  console.log(count)
}, [])

常见写法大概两种:

不写依赖数组(每次渲染后都会执行一次):

jsx 复制代码
useEffect(() => {
  // 组件初始化 + 每次更新都会执行
})

写依赖数组(依赖变了才执行):

jsx 复制代码
useEffect(() => {
  // count 变化时才执行
}, [count])

到这里,这几个常见疑问其实都能落到同一个答案:

  • React 取 Hooks 状态,靠的是 Fiber 上那条 按顺序排好的 Hook 链表
  • 你调用 setState 的时候,React 先把"要怎么改"塞进 updateQueue,等这一轮调度真正跑起来再统一算。
  • effect 回调里拿到的变量,本质上来自"某一次渲染生成的闭包",依赖数组写漏了,就可能一直用的是旧那次。

把这三点抓住,后面的学习才会行稳致远

相关推荐
一颗小行星2 小时前
Harness Engineering 前端开发的下一个阶段
前端·ai编程
踩着两条虫2 小时前
重磅!这款AI低代码平台火了:拖拽生成应用,一键输出Web/H5/UniApp
前端·低代码·ai编程
我命由我123452 小时前
Vite - Vite 最小项目
服务器·前端·javascript·react.js·ecmascript·html5·js
布局呆星2 小时前
Vue3 | 事件绑定与双向数据绑定
前端·javascript·vue.js
Hilaku2 小时前
前端资质越高,越来越不敢随便升级框架?
前端·javascript·架构
自然常数e2 小时前
预处理讲解
java·linux·c语言·前端·visual studio
@菜菜_达2 小时前
Vue 入门学习
前端·vue.js·学习
终端鹿2 小时前
手写 Vue3 自定义指令:防抖、点击外部、权限控制
前端·javascript·vue.js
洗发水很好用2 小时前
uniapp纯css实现基础多选组件
前端·css·uni-app