React 内部机制:渲染模型、State 与 Hooks 基础

Dan Abramov 系列笔记——从渲染快照到 Fiber 架构,搞懂 React 是怎么工作的

1. 每次渲染都是独立的快照

  • count 只是一个数字,不是“数据绑定”

  • 每次 setState → 重新调用组件函数 → 新的 count

  • 闭包捕获的永远是当时的值,不会变

// 第一次渲染:count = 0
// 第二次渲染:count = 1
// 第三次渲染:count = 2
// 每个渲染的 count 是独立的常量

2. 函数组件 vs 类组件

核心区别:函数组件用闭包,类组件用 this

函数组件 类组件
读取值 闭包捕获当时的值 this.props / this.state 读最新值
数据一致性 天然保证(闭包不可变) 需要手动管理
异步操作 捕获的值永远正确 可能读到“未来”的值
// 函数组件:闭包捕获 user = 'Dan'
function ProfilePage({ user }) {
  const showMessage = () => alert('Followed ' + user) // 永远是 Dan
}

// 类组件:this.props 可变
class ProfilePage extends React.Component {
  showMessage = () => alert('Followed ' + this.props.user) // 可能变成 Sophie
}

3. React as a UI Runtime(渲染模型)

三层架构:

JSX → React Element(轻量描述对象)→ Reconciler(Fiber 协调)→ Host Tree(DOM)

核心流程:

渲染(Render):调用组件函数,生成 React Element 树(纯计算,不操作 DOM)
协调(Reconcile):对比新旧 Fiber 树,找出差异
提交(Commit):把差异应用到真实 DOM

协调规则:

  • 同一位置 \+ 同类型 → 复用

  • 类型变了 → 销毁重建

  • key 让 React 按身份匹配,不按位置

组件返回值:

  • React Element → 描述 UI

  • null → 不渲染

  • 函数 → effect 的 cleanup

你的 JSX 写 <div className="card">hello</div>,React Element 层产出 { type: 'div', props: { className: 'card', children: 'hello' } }

然后 Host Renderer 看到 type: 'div'——

  • react-dom 说:哦 div,我去调 document.createElement('div')

  • react-native 说:不认识 div,报错(你得写 <View>

  • react-three-fiber 说:不认识 div,报错(你得写 <mesh>

4. Hooks 调用顺序

React 用调用顺序标识每个 Hook 的状态:

function Form() {
  const [name, setName] = useState('Mary')      // #1
  const [surname, setSurname] = useState('Poppins') // #2
  const [width, setWidth] = useState(window.innerWidth) // #3
}

为什么用调用顺序:

  • 不需要命名 → 无冲突

  • 复制粘贴安全

  • 自由组合,无钻石问题

  • 每次渲染形成树状调用栈,天然隔离

代价: Hooks 必须顶层无条件调用,不能放在 if/for 里

5. useEffect 依赖数组

核心:effect = 声明式同步,deps = 同步触发条件,cleanup = 同步终止条件

useEffect(() => {
  const ws = new WebSocket(url)
  return () => ws.close()  // cleanup
}, [url])                   // url 变了 → 先销毁旧同步,再建立新同步

deps 的规则:

  • useEffect(fn) → 没传 deps → 每次渲染都执行

  • useEffect(fn, []) → 空数组 → 只在挂载时执行(依赖没变所以跳过)

  • useEffect(fn, [a, b]) → a 或 b 变了才执行

跟 Vue 的对比:

React:手动声明依赖 + 手动写 cleanup
Vue:编译器自动追踪依赖 + 自动 cleanup
本质一样:响应式 + 副作用管理

6. setInterval + Hooks(声明式 vs 命令式)

阻抗失配: React 声明式(每次渲染独立快照),setInterval 命令式(回调固定)

解法:useRef 穿透闭包

function useInterval(callback, delay) {
  const savedCallback = useRef()

  // 每次渲染后更新最新的 callback
  useEffect(() => {
    savedCallback.current = callback
  })

  // interval 只创建一次,调用的是 ref 里的最新 callback
  useEffect(() => {
    function tick() { savedCallback.current() }
    if (delay !== null) {
      let id = setInterval(tick, delay)
      return () => clearInterval(id)
    }
  }, [delay])
}

useRef 的本质: 跨渲染共享可变值,类组件 this 的等价物

7. setState / useState 原理(依赖注入)

react 包只暴露 API,实现全在 renderer**

// 类组件:this.updater 由 renderer 注入
setState(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback)
}

// Hooks:__currentDispatcher 由 renderer 注入
useState(initialState) {
  return React.__currentDispatcher.useState(initialState)
}

依赖注入: renderer 在渲染前把 updater/dispatcher 注入,组件通过它们跟 renderer 通信

这也是为什么 reactreact-dom 版本必须匹配

8. React 如何区分类/函数

React.Component.prototype.isReactComponent = {}

// React 判断逻辑
function isClassComponent(fn) {
  return fn.prototype && fn.prototype.isReactComponent
}

原理: 沿原型链查找 isReactComponent

  • 找到 → 类组件 → new Greeting(props).render()

  • 没找到 → 函数组件 → Greeting(props)

9. Fiber 架构

让渲染可中断、可恢复。

Fiber 就是一个 JS 对象,描述当前 UI 的状态

{
  tag: 'FunctionComponent',
  type: Component,
  pendingProps: {},
  memoizedProps: {},
  memoizedState: null,     // hooks 链表挂在这里
  child: Fiber,
  sibling: Fiber,
  return: Fiber,
  lanes: 0,               // 优先级
  alternate: Fiber,        // 双缓冲
}

核心机制:

  • 双缓冲:current(屏幕)↔ workInProgress(构建中),构建完成后切换指针

  • lanes 优先级:位掩码,高优先级任务(用户点击)可以打断低优先级

  • hooks 链表:memoizedState → 链表,按调用顺序访问

10. Before You memo(性能优化)

先拆分组件结构,再用 memo

技巧 1:状态下移

// 把 state 和依赖它的 UI 抽成子组件
function App() {
  return (
    <>
      <Form />           {/* state 在这里面 */}
      <ExpensiveTree />  {/* 不受影响 */}
    </>
  )
}

技巧 2:内容上提

// 通过 children 传不变的内容
function App() {
  return (
    <ColorPicker>        {/* state 在这里面 */}
      <ExpensiveTree />  {/* children 引用不变,React 跳过 */}
    </ColorPicker>
  )
}

原理: React 协调只比较同一位置的类型和引用。children 引用不变 → 跳过子树

11. Development Mode

process.env.NODE_ENV 是构建时字符串替换,不是运行时变量**

// 构建后
if ('production' !== 'production') {  // false → 删除
  doSomethingDev()
} else {
  doSomethingProd()  // 保留
}

React 策略: npm 包预编译两份 bundle(dev \+ prod),入口文件用一次判断选择加载哪个

开发模式为什么慢: 额外 warning \+ StrictMode 双重渲染 \+ 详细错误追踪

12. 工程哲学

let vs const: 不值得争论,团队统一就好

Clean Code: 不是目标,是防御机制。代码的价值在于“改起来多容易”,不是重复行数

Let clean code guide you. Then let it go.

底层一句话

React 没有魔法。闭包、原型链、依赖注入、编译时替换——全是 JS 基础。写不好 React = 基础不够扎实。


来源:react.dev/learn/state-as-a-snapshot

核心观点:setState 不会立即改变当前渲染的 state,而是触发下一次渲染

setState 是"拍照",不是"改值"
当前渲染的 state 永远是固定的,不会因为你调了 setState 就变

1. setState 触发重新渲染,不是立即改变值

function Counter() {
  const [number, setNumber] = useState(0)

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1)
        setNumber(number + 1)
        setNumber(number + 1)
      }}>+3</button>
    </>
  )
}

点击后 number 只加 1,不是 3。 因为当前渲染的 number 是 0,三次 setNumber(number + 1) 实际上都是 setNumber(0 + 1)

2. 快照替换法

每次渲染都是一张快照。把事件处理器里的 state 变量替换成它的值:

// 当前渲染 number = 0
<button onClick={() => {
  setNumber(0 + 1)  // → setNumber(1)
  setNumber(0 + 1)  // → setNumber(1)
  setNumber(0 + 1)  // → setNumber(1)
}}>+3</button>

// 下次渲染 number = 1
<button onClick={() => {
  setNumber(1 + 1)  // → setNumber(2)
  setNumber(1 + 1)  // → setNumber(2)
  setNumber(1 + 1)  // → setNumber(2)
}}>+3</button>

3. 异步代码也看到的是“当时的”快照

function Counter() {
  const [number, setNumber] = useState(0)

  return (
    <button onClick={() => {
      setNumber(number + 5)
      setTimeout(() => {
        alert(number)  // 显示 0,不是 5
      }, 3000)
    }}>+5</button>
  )
}

即使 3 秒后组件已经重新渲染了,alert 看到的仍然是“当时”的 number = 0。 因为事件处理器闭包捕获的是那次渲染的快照。

4. 同一事件处理器内,所有 setState 看到的是同一个快照

// 不管你在事件处理器里调了多少次 setState
// 它们读到的 state 值都是"这次渲染"的值
// 这就是为什么连续三次 setNumber(number + 1) 只加了 1

5. 如果你想基于“最新”state 更新,用函数式更新

// 🔴 三次都是 setNumber(0 + 1)
setNumber(number + 1)
setNumber(number + 1)
setNumber(number + 1)

// ✅ 三次分别是 1, 2, 3
setNumber(n => n + 1)
setNumber(n => n + 1)
setNumber(n => n + 1)

函数式更新 setNumber(n => n + 1) 不依赖当前渲染的快照,而是基于队列中最新的值。

核心一句话