2026年了,快来领取你的第一篇react工作原理,面试必备,拷打面试官

40 阅读22分钟

1. 概述

本文是基于 react v19 而写的一篇 react 工作原理文档,后续将更新一篇 如何 0-1 实现 mini-react 的文,大家感兴趣的话,也可以期待一下。

1.1. 整体架构

四大核心包的职责:

包名路径职责
reactpackages/react定义组件 API、Hooks、createElement、JSX Runtime
react-reconcilerpackages/react-reconcilerFiber 架构核心,Diff 算法,协调更新
react-domnpackages/react-domDOM 渲染器,事件系统,浏览器宿主操作
schedulerpackages/scheduler任务优先级调度,时间切片

1.1.1. 核心数据结构:fiber

Fiber 是 React 的核心数据结构,每个 React 元素对应一个 fiber 节点,fiber 的数据结构如下:

FiberNode {
  // 身份标识
  tag: WorkTag,              // 组件类型 (FunctionComponent=0, ClassComponent=1, HostComponent=5...)
  key: null | string,
  elementType: any,          // createElement 的第一个参数
  type: any,                 // 解析后的类型 (function/class)
  stateNode: any,            // 指向真实实例

  // Fiber 树结构 (链表树)
  return: Fiber | null,      // 父节点
  child: Fiber | null,       // 第一个子节点
  sibling: Fiber | null,     // 下一个兄弟节点
  index: number,

  // 状态
  pendingProps: any,         // 新的 props
  memoizedProps: any,        // 上次渲染的 props
  memoizedState: any,        // 上次渲染的 state (Hooks 链表头)
  updateQueue: mixed,        // effect副作用的更新队列

  // 副作用
  flags: Flags,              // 副作用标记 (Placement, Update, Deletion...)
  subtreeFlags: Flags,       // 子树副作用标记 (冒泡优化)
  
  // 双缓冲
  alternate: Fiber | null,   // 指向另一棵树的对应节点 (current ↔ workInProgress)
  
  // 优先级
  lanes: Lanes,              // 本节点的优先级,也用于标记fiber节点是否有更新
  childLanes: Lanes,         // 子树的优先级,也用于标记子树fiber节点是否有更新
}

1.1.2. fiber 树遍历

  1. fiber 本质上是一颗链表树(左孩子右兄弟链表编码的树),逻辑结构是树,物理实现是链表,遍历方式是链表的遍历,不采用递归,所以可以随时暂停和恢复(ps: 递归之所以不可以暂停,是因为调用栈保存在 js 引擎中,无法在 js 层面保存和恢复这个调用栈)
  2. fiber 树的遍历采用深度优先遍历,每一步都可中断,例子如下:
<App>
  <Header />
  <Main>
    <Article />
    <Sidebar />
  </Main>
  <Footer />
</App>

对应的fiber链表树:
           App
            │ child
            ▼
         Header ──sibling──▶ Main ──sibling──▶ Footer
                               │ child
                               ▼
                           Article ──sibling──▶ Sidebar

         所有节点的 return 指针:
         Header.return  = App
         Main.return    = App
         Footer.return  = App
         Article.return = Main
         Sidebar.return = Main

遍历顺序 (performUnitOfWork):
1. beginWork(App)       → 返回 child = Header
2. beginWork(Header)    → 返回 null (无子节点)
3. completeWork(Header) → 有 sibling = Main
4. beginWork(Main)      → 返回 child = Article
5. beginWork(Article)   → 返回 null
6. completeWork(Article)→ 有 sibling = Sidebar
7. beginWork(Sidebar)   → 返回 null
8. completeWork(Sidebar)→ 无 sibling, returnMain
9. completeWork(Main)   → 有 sibling = Footer
10. beginWork(Footer)   → 返回 null
11. completeWork(Footer)→ 无 sibling, returnApp
12. completeWork(App)   → 完成

这就是 Fiber "可中断递归" 的基础 — 任何一步都可以暂停,因为当前处理到哪个节点、下一步该去哪里,都由 child/sibling/return 指针决定,不依赖 js 调用栈。

2. 核心流程

2.1. 总体流程

 用户操作 (click/input/...)
       │
       ▼
 ① 触发更新 (Trigger)
    setState() / dispatch()
       │
       ▼
 ② 创建 Update 对象 { action, lane, next }
       │
       ├─ 挂到对应 hook.queue.pending(环形链表)
       │    (dispatch 通过闭包绑定了具体的 fiber 和 queue,不会挂错)
       ▼
 ③ 标记 Lane 优先级,从触发节点向上冒泡到 FiberRootNode
       │
       ▼
 ④ 调用 ensureRootIsScheduled() 进入调度器
       │
       ▼
 ⑤ Scheduler 根据优先级调度回调
    │                              
    │  SyncLaneMicroTask  最高优先级  performSyncWorkOnRoot 比如点击/输入触发的更新
    │  DefaultLaneMessageChannel	 默认优先级  performConcurrentWorkOnRoot
    │  		           宏任务不用setTimeout实现,有延时,messageChanel可以做到0延迟宏任务
    │  TransitionLane → 低优先级   performConcurrentWorkOnRoot
    │                              
    ▼
 ⑥ ═══════ Render 阶段 (可中断) ═══════ (在内存中构建一颗新的fiber树,标记所有需要的dom变更)
    │
    │  performSyncWorkOnRoot()  /  performConcurrentWorkOnRoot()
    │       │
    │       ▼
    │  根据lanes,判断是 重新创建workInProgress树,还是 断点续传
    │  	 	prepareFreshStack()  →  创建workInProgress树首节点
    │       │
    │       ▼
    │  workLoopSync() / workLoopConcurrent() (深度优先遍历)
    │       │
    │       │ performUnitOfWork(workInProgress); 工作单元
    │       │
    │       ├──▶ beginWork(current, wip, lanes)  ← 递 (向下)
    │       │       │  bailout,判断节点是否需要跳过
    │       │       ├─ 根据 fiber.tag 分发处理
    │       │       │   ├─ FunctionComponent -> renderWithHooks
    │       │       │   ├─ ClassComponent			│
    │       │       │   ├─ HostComponent			▼
    │       │       │   └─ ...											
    │       │       │																
    │       │       ├─ Diff 算法 (reconcileChildFibers)(diff当前fiber的直接子级) 
    │       │       │   ├─ 单节点 diff
    │       │       │   └─ 多节点 diff (两轮遍历)
    │       │       │
    │       │       └─ 返回 child fiber 或 null
    │       │
    │       └──▶ completeWork(current, wip, lanes)  ← 归 (向上)
    │               │
    │               ├─ 创建/更新 DOM 实例 (不挂载)
    │               ├─ flags 冒泡
    │               └─ 沿 return 指针向上
    │
    │  完成后得到一棵带 flags 标记的 workInProgress Fiber 树
    │
    ▼
 ⑦ ═══════ Commit 阶段 (同步不可中断) ═══════
    │
    │  commitRoot(root)
    │       │
    │       ├─── Before Mutation 阶段
    │       │
    │       ├─── Mutation 阶段 (操作真实 DOM)
    │       │      
    │       ├─── 切换 current 指针: root.current = finishedWork
    │       │
    │       └─── Layout 阶段
    │
    ▼
 ⑧ 浏览器绘制 (Paint)
    │
    ▼
 ⑨ useEffect 异步执行

2.2. 触发阶段

2.2.1. 创建 Update 对象

Update 对象就是一个记录"要做什么改变"的数据包,挂到对应 hook 的 queue 上,数据结构如下:

  // Update 对象
  const update = {
    lane: lane,            // 这个更新的优先级
    action: action,        // 你传的值:1 或 (prev) => prev + 1
    hasEagerState: false,  // 是否有提前计算的结果
    eagerState: null,      // 提前计算的结果
    next: null,            // 环形链表指针
  };


其中queue为环形链表,假如setCount了三次,那么结构分别如下:

setCount(1):
  queue.pending → A{action:1} ↺

setCount(2):
  queue.pending → B{action:2} → A{action:1} → B ↺

setCount(3):
  queue.pending → C{action:3} → A{action:1} → B{action:2} → C ↺
                  ↑                                           │
                  └───────────────────────────────────────────┘

pending 指向最后插入的 C
C.next 指向第一个 A
render阶段消费时,从 pending.next 开始 → A → B → C(插入顺序)

2.2.2. lanes 冒泡

标记 lane 就是在 fiber 上记录"我有这个优先级的更新要处理",冒泡标记父级 fiber 的childLanes就是告诉父节点 fiber "我下面有更新"。

scheduleUpdateOnFiber(fiber, lane)
│
├─ 标记当前 fiber
│    fiber.lanes = fiber.lanes | SyncLane
│    (位运算合并,一个 fiber 可以同时有多个 lane)
│
└─ 冒泡:沿 return 指针往上走,标记每个祖先的 childLanes
     let parent = fiber.return
     while (parent !== null) {
       parent.childLanes = parent.childLanes | SyncLane
       parent = parent.return
     }

举个例子:
setCount(1) 在 Counter 组件触发,lane = SyncLane

标记前:
  HostRoot  { lanes: 0, childLanes: 0 }
    App     { lanes: 0, childLanes: 0 }
      Counter { lanes: 0, childLanes: 0 }  ← setState 发生在这里
        div   { lanes: 0, childLanes: 0 }

标记后:
  HostRoot  { lanes: 0, childLanes: SyncLane }  ← 冒泡到这里
    App     { lanes: 0, childLanes: SyncLane }  ← 冒泡到这里
      Counter { lanes: SyncLane, childLanes: 0 } ← 标记自己
        div   { lanes: 0, childLanes: 0 }

冒泡的目的: 为了给 render 阶段 workLoop 工作循环的 beginWork 指路,判断是否子树有更新

render 阶段 workLoop 从 HostRoot 开始往下走:

beginWork(HostRoot)
  自己 lanes = 0 → 不需要处理自身
  childLanes 有 SyncLane → 子树有活 → 往下走 ✓

beginWork(App)
  自己 lanes = 0 → bailout(不执行 App 函数)
  childLanes 有 SyncLane → 子树有活 → 继续往下 ✓

beginWork(Counter)
  自己 lanes 有 SyncLane → 有更新!→ 执行 Counter() ✓

beginWork(div)
  自己 lanes = 0,childLanes = 0 → 全跳过 ✓

2.3. scheduler 调度

理论上任何可能产生更新的地方都会调用ensureRootIsScheduled

setState / forceUpdate
    └─ scheduleUpdateOnFiber
         └─ ensureRootIsScheduled  ← 每次状态更新后

commitRoot(提交完成后)
    └─ ensureRootIsScheduled       ← 检查是否还有剩余工作

flushPassiveEffects(useEffect 执行后)
    └─ ensureRootIsScheduled       ← effect 中可能产生了新更新
ensureRootIsScheduled(root)
  │
  ├─ ① 已经派过同优先级的活了吗?
        └─ 是 → 不重复派,return(这就是自动批处理的基础)
                用户连续点击 setState 三次(同一事件中):
                setCount(1)  → ensureRoot → 没有已有调度 → 派活(微任务)
                setCount(2)  → ensureRoot → 已有同优先级 → returnsetCount(3)  → ensureRoot → 已有同优先级 → return ✓
                事件结束 → 微任务执行 → 一次性渲染(三个 update 一起消费)
              
            
        └─ 否 → 
                transition 渲染中(低优 TransitionLane) → 用户点击(高优 SyncLane)
                ensureRoot 发现:
                  已有调度 = TransitionLane
                  新优先级 = SyncLane
                  不同!→ 取消旧的任务 → 派新的高优任务
                        (丢弃旧wip树是在render阶段开始时执行)
  
  └─ ② 怎么派?
      ├─ SyncLaneMicroTaskperformSyncWorkOnRoot(root)  点击/输入/flushsync
      └─ 其他 LaneMessageChannelperformConcurrentWorkOnRoot(root) useEffect
    
两个入口最终都进入 render:

performSyncWorkOnRoot(root)             performConcurrentWorkOnRoot(root)
  ├─ 不可中断           										├─ 可中断	
  ├─ renderRootSync(root, lanes)          ├─ renderRootConcurrent(root, lanes)
  │   ├─ prepareFreshStack()              │   ├─ prepareFreshStack()
  │   └─ workLoopSync()                   │   └─ workLoopConcurrent()
  │                                       │
  └─ commitRoot(root)                     ├─ 检查结果:
                                          │   ├─ RootCompletedcommitRoot(root)
                                          │   └─ RootInProgress → 等下次调度继续
                                          └─ (可能被更高优先级中断)

2.4. render 阶段

2.4.1. 重新创建 wip 树 / 断点续传

每次 render 开始前,会先判断 创建新的 wip 树 / 旧 wip 树断点续传,prepareFreshStack 函数用于创建 wip 树的首节点 HostRoot fiber。

renderRootSync(root, lanes)  / renderRootConcurrent(root, lanes)
│
├─ 当前是否有进行中的 wip 树?
│   ├─ 没有 → prepareFreshStack (新建 wip 树)
│   └─ 有进行中的 wip 树:
│       ├─ 本次任务 lanes !== 正在进行的 lanes? (例如高优插队)
│       │   └─ 是 → prepareFreshStack (丢弃旧树,重新开始)
│       │
│       └─ 本次任务 lanes === 正在进行的 lanes? (断点续传)
│           └─ 跳过 prepareFreshStack,直接继续 workLoop
│              │
│              └─ React19 核心改变(react18采用双队列来交错更新,复杂且极端场景容易出bug)
│                 如果此时触发了新的同优先级 setState:
│                 1. React 会把新 update 暂存,暂不干扰当前 wip 树。
│                 2. workLoop 继续跑完当前的半成品树,直到渲染完成。
│                 3. 渲染循环结束后,调用 finishQueueingConcurrentUpdates,把暂存的 update 正式挂到 Fiber 树上。
│                 4. 在 commit 阶段后,由 ensureRootIsScheduled 为这些遗留的 update 再次开启一轮全新的渲染。

2.4.2. 工作循环(workLoopSync / workLoopConcurrent)

// 同步模式: 点击、输入、flushSync等走这条路
function workLoopSync() {
  // 没有任何判断,一口气干完,创建好本次更新后的完整wip树
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// 并发模式: 每个单元后检查时间
function workLoopConcurrent() {
  // 每处理一个 fiber,就检查一次:时间片用完了吗?
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

// 处理fiber的工作单元
function performUnitOfWork(unitOfWork: Fiber) {
  const current = unitOfWork.alternate;
  
  // 递:处理当前节点,返回它的第一个child
  const next = beginWork(current, unitOfWork, renderLanes);
  
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  
  if (next !== null) {
    // 有 child → 继续往下走
    workInProgress = next;
  } else {
    // 没 child → 到底了,开始往上走
    completeUnitOfWork(unitOfWork);
  }
}

function completeUnitOfWork(unitOfWork: Fiber) {
  let completedWork = unitOfWork;
  
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    
    // 归:创建/更新 真实DOM节点,flags冒泡
    completeWork(current, completedWork, renderLanes);
    
    // 有兄弟?→ 兄弟成为下一个 workInProgress,退出归阶段
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;  // ← 回到 workLoopSync,对 sibling 开始新一轮"递"
    }
    
    // 没兄弟?→ 继续往上归
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
}

2.4.3. 递阶段和归阶段(beginWork 和 compeleWork)

2.4.3.1. beginWork
2.4.3.1.1. bailout 判断

判断是否可以跳过,需要满足三个条件才可以跳过

beginWork(current, workInProgress, renderLanes)
│
│ ═══ 第一步: 判断是否可以跳过 ═══
│
├─ current !== null?  (有旧 fiber = update 阶段)
│   │
│   │  const oldProps = current.memoizedProps;
│   │  const newProps = workInProgress.pendingProps;
│   │
│   │  三个条件全部满足才可能 bailout:
│   │
│   │  ① oldProps === newProps               (props 引用没变)
│   │  ② !hasLegacyContextChanged()          (旧版 context 没变)
│   │  ③ !includesSomeLane(                  (本节点没有待处理的更新)
│   │       workInProgress.lanes, renderLanes)
│   │       lanes用来标记有没有更新,是什么优先级,具体的更新内容在更新队列上
│   │
│   ├─ 全满足
│   │   │
│   │   └─ bailoutOnAlreadyFinishedWork(current, wip, renderLanes)
│   │       │
│   │       ├─ wip.childLanes 子树是否有更新
│   │       │   ├─ YEScloneChildFibers(current, wip)
│   │       │   │        return wip.child
│   │       │   │        ↑ 本节点跳过,但子树有更新,克隆后继续向下
│   │       │   │
│   │       │   └─ NOreturn null
│   │       │            ★ 整棵子树全部跳过!★
│   │       │            这是 React 最强力的性能优化
│   │       │
│   │       └─ cloneChildFibers 做什么:
│   │              不重新 reconcile,而是直接复制            
│   │
│   └─ 不全满足 → didReceiveUpdate = true → 进入完整处理
│
├─ current === null?  (首次渲染,无旧 fiber)
│   → 直接进入完整处理 (不可能 bailout)
2.4.3.1.2. 按 tag 分发处理
│ ═══ 第二步: 根据 tag 执行具体逻辑 ═══
│
switch (workInProgress.tag) {
  case FunctionComponent: {
    const Component = workInProgress.type;
    const nextProps = workInProgress.pendingProps;
    return updateFunctionComponent(
      current, workInProgress, Component, nextProps, renderLanes
    );
  }

  case ClassComponent: { ... }
  case HostComponent: { ... }
  ...
}

// 函数组件处理流程
updateFunctionComponent(current, wip, Component, {}, renderLanes)
  │
  ├─ renderWithHooks:
  │   ├─ 重置 wip.memoizedState / updateQueue / lanes
  │   │       // hook 链表清空    effect 链表清空     
  │   ├─ 选择 dispatcher 	 首次渲染 和 更新渲染的行为完全不同
  │   └─ Component(props)  ->  执行组件函数,hooks运行,返回children
  │
  └─ reconcileChildFibers(returnFiber, currentFirstChild, newChild) // diff过程
      │
      ├─ newChild 是单个元素(object, $$typeof === REACT_ELEMENT_TYPE)
      │   → reconcileSingleElement()
      │       ├─ 遍历旧 children,找 key 和 type 都匹配的
      │       │   ├─ 找到 → useFiber() 复用旧 Fiber,更新 props,删除其余 sibling
      │       │   └─ 没找到 → createFiberFromElement() 创建新 Fiber
      │       └─ 多余的旧节点 → 标记 Deletion
      │
      ├─ newChild 是数组(多个子元素)
      │   → reconcileChildrenArray()
      │       ├─ 第一轮:从左到右逐个比较(fiber更新的频率最多,第一轮主要处理要更新的节点)
      │       │   ├─ key 相同 + type 相同 → 复用,继续
      │       │   ├─ key 相同 + type 不同 → 删旧建新,继续
      │       │   └─ key 不同 → 停下来,进入边界判断/第二轮
      │       ├─ 旧的遍历完了? → 剩下的新元素全部 create,标记 Placement
      │       ├─ 新的遍历完了? → 剩下的旧 Fiber 全部标记 Deletion
      │       └─ 第二轮:把剩余旧节点放入 Map<key, Fiber>
      │           → 遍历剩余新元素,从 Map 中找可复用的
      │           → 找到 → 复用,oldIndex < lastPlacedIndex 才标记移动
      │           → 没找到 → 新建,标记 Placement
      │           → Map 中剩余的 → 全部标记 Deletion
      │
      ├─ newChild 是文本/数字
      │   → reconcileSingleTextNode()
      │
      └─ newChild 是 null/undefineddeleteRemainingChildren()
2.4.3.1.3. renderWithHooks
  1. 重置 hooks/副作用/lanes 状态
    1. React 的 hooks 是一条单向链表,挂在 fiber.memoizedState 上。每次执行组件函数时,每个 useState / useEffect 调用都会创建一个新的 hook 节点,按调用顺序串成新链表。
    2. 重置是为了从头开始构建这条新 hooks 链表。
current fiber (旧树)          wip fiber (新树)
memoizedState                 memoizedState
  ↓                             ↓
[hook0] → [hook1] → [hook2]   [hook0] → [hook1] → [hook2]
  ↑                             ↑
currentHook (读旧值)          workInProgressHook (写新值)

在执行函数组件时,每次调用一个 hook(比如 useState),都会创建新的 hook 节点,把旧状态拷过来,再应用 pending 的 update queue,把新 hook 挂到 wip.memoizedState 链表上

即:
        链表结构		    状态值 
Mount		从零创建  	用 initialState
Update	从零重建	  从currentHook.memoizedState 读取 + 处理 queue 更新队列
  1. 选择 dispatcher
    1. 首次渲染和更新渲染的行为是不同的,需要采用不同的 dispatcher 去处理
// 首次渲染(Mount)
HooksDispatcherOnMount = {
  useState:   mountState,      // 创建 hook 节点,初始化 state
  useEffect:  mountEffect,     // 创建 effect,标记需要执行
  useRef:     mountRef,        // 创建 { current: initialValue }
  useMemo:    mountMemo,       // 计算并缓存
  ...
};

// 更新渲染(Update)
HooksDispatcherOnUpdate = {
  useState:   updateState,     // 读取 hook 节点,处理 queue.pending 环形链表
  useEffect:  updateEffect,    // 对比依赖,决定是否重新执行
  useRef:     updateRef,       // 直接返回已有的 ref
  useMemo:    updateMemo,      // 对比依赖,决定是否重新计算
  ...
};
  1. 执行函数组件,重建 hooks 链表,返回 children

自上而下运行函数组件

1. useState
  a. mount → 创建 Hook,初始化 state,绑定 dispatch
  b. update → 取出对应 Hook,消费 queue.pending 中的 Update 链表,算出新 state

2. useEffect
  a. mount → 创建 Hook,创建 Effect { create, destroy, deps, tag: HookHasEffect }
  b. update → 取出对应 Hook,对比 deps
      1. 变了 → Effect.tagHookHasEffect(要执行)
      2. 没变 → Effect.tag 不带(跳过)
      3. hook.memoizedState = effect
      4. pushEffect → 追加到 fiber.updateQueue 环形链表
      5. fiber.flags |= Passive

3. 返回 children(ReactElement 对象树)
组件代码:                     hooks 链表 (memoizedState):

const [count, setCount]      hook0: { memoizedState: 0, queue: {}, next: → }
  = useState(0);                                                                                                               
useEffect(() => {            hook1: { memoizedState: effectObj, next: null }
  document.title = count;
}, [count]);

return (
  <button onClick={() => {
    setCount(1);  待更新update
  }}>
    {count}
  </button>
);

Fiber (<Componnet />) // 用户点击了一次按钮 setCount(1),从触发到render阶段hooks变化
  │
  │  fiber的memoizedState存储的是hooks链表的头节点
  │  state hook的更新操作存储在对应的 hook.queue 里面
  │  effect 分别存储在对应的 hook.memoizedState 和 fiber.updateQueue 里
  │
  └── memoizedState
      ──► Hook 0 (useState)
      // trigger阶段
      memoizedState: 0      ← state 值
      queue: {
        // 在trigger阶段产生Update对象追加在此
        pending ──► Update { action: 1, lane: SyncLane }
      }                                
      // ——> render阶段,即执行组件函数 Component(props) ,重建hooks链表时候
      memoizedState: 1      ← state 新值
      queue: {
        pending ──► null
      }
      next ──► Hook 1 (useEffect)
                 memoizedState      ──►   Effect {
                 // trigger阶段      ──►   render阶段
                 tag: HookHasEffect ──►   tag: HookHasEffect
                 deps: [0]          ──►   deps: [1]  ✓ 新 deps
                 destroy: undefined ──►   destroy: fn  ✓ 上轮返回的清理函数
                 } 
                 next ──► null

  └── updateQueue──► render阶段不消费,重新执行一遍组件函数,useEffect就会重新pushEffect
                          ├── memoizedState: effect 对象
                          └── next ──► null

// 时间线变化
                    MountTriggerRender 后(wip)
                    ────────          ──────────         ─────────────
hook.queue
  .pending          null              Update{1} ↺       null(已消费)

fiber
  .updateQueue      Effect(deps:[0])  Effect(deps:[0])  Effect(deps:[1])
                    ↑                 ↑                  ↑
                    mount 时创建       没动过              清空后重建


注意⚠️:点击setCount一次,那么创建的update对象,是如果知道自己应该挂载在哪个hook的queue里呢?
答案:是闭包,setCount在创建时就已经记住了在哪个fiber的哪个hook节点里。

注意⚠️: useEffect的存储位置为两个

存储位置谁用用来干嘛
Hook 链表 (fiber.memoizedState)render 阶段按顺序匹配 hook,对比 deps 决定这轮 effect 要不要重新执行
fiber.updateQueuecommit 阶段快速遍历所有需要执行的 effect,不用再走一遍 hook 链表
reconcileChildFibers

diff 过程,为新的 ReactElement 和旧的 Fiber 的对比,此时这里仅 diff 当前 fiber 的直接子级。

1.新的 ReactElement 和旧的 Fiber 的对比:
新(组件函数刚执行返回的)            旧(current 树上已有的)
─────────────────────              ─────────────────
ReactElement                       Fiber
  { type: 'span',                    { type: 'span',
    props: { children: 1 },           memoizedProps: { children: 0 },
    key: null }                        key: null }

       │                                  │
       └──────── 比较 type 和 key ─────────┘
                      │
              相同 → 复用 Fiber,用新 props 更新
              不同 → ...

2.仅 diff 当前 fiber 的直接子级
<App>           ← beginWork(App) 时 diff 这一层的 children
  <Header />         ↑ 只对比 App 的直接子级
  <Main>             ↑ HeaderMainFooter
    <Article />      ✗ 不管,等 beginWork(Main) 时再 diff
    <Sidebar />      ✗ 不管
  </Main>
  <Footer />         ↑
</App>

而逐层 diff,靠 workLoop 工作循环来推进,举个例子:

function App() {
  const [count, setCount] = useState(1);
  return (
    <div>
      <span>{count}</span>
      <button>+1</button>
    </div>
  );
}

假设 count 从 0 变成 1beginWork(App)
  执行 App() → 返回 Element { type: 'div', children: [span, button] }
  reconcile: 旧 div fiber vs 新 div element → type和key相同 → 复用,更新 props
  返回 wip div fiber ↓

beginWork(div)
  HostComponent,不执行组件函数
  从 pendingProps.children 取出 [span element, button element]
  reconcile 多节点 diff:
    span fiber  vs span element  → key和type相同 → 复用, props 变了(children: 01)
    button fiber vs button element → key和type相同 → 复用, props 没变
  返回 wip span fiber ↓

beginWork(span)
  HostComponent,pendingProps.children = 1(新)vs memoizedProps.children = 0(旧)
  文本变了 → fiber.flags |= Update
  返回 null → completeUnitOfWork 开始 ↑

每次 beginWork 只 diff 当前 fiber 的直接子级(一层),
整棵树的 diff 是 workLoop 逐层推进、多次 beginWork 累积的结果。

更为具体的diff流程,详见下述:

2.4.3.2. completeWork

completeWork 是归阶段的核心,主要是创建/更新真实 dom 节点(但不挂载到页面上去),收集 flags 向上冒泡。

2.4.3.2.1. 创建/更新 dom 节点
completeWork(current, workInProgress, renderLanes)
│
├─ HostComponent(div, span 等原生标签)
│   │
│   ├─ mount(current === null):
│   │    ├── document.createElement('div')     ← 创建真实 DOM
│   │    ├── 把所有子 DOM 节点 append 进去       ← 组装 DOM 树
│   │    ├── 设置 props(className, style, 事件等)
│   │    └── fiber.stateNode = dom              ← 存到 fiber 上
│   │
│   └─ update(current !== null):
│        ├── DOM 已存在(fiber.stateNode 有值)
│        ├── 对比 oldProps vs newProps
│        │    ├─有变化 → 收集到 fiber.updateQueue = ['className', 'new-class', ...]
│        │    │         			fiber.flags |= Update// HostComponent(div, span 等)的 updateQueue 与 FunctionComponnets 存储内容不同
│        │    └─ 没变化 → 什么都不做
│        └── 不创建新 DOM,不操作 DOM(等 commit 阶段)
│
├─ HostText(文本节点)
│   ├─ mount → document.createTextNode(text)
│   └─ update → 标记 Update(commit 时改 nodeValue)
│
├─ FunctionComponent
│   └─ 基本什么都不做(没有 DOM 要创建)
│
└─ 其他 tag → ...
2.4.3.2.2. Flags 冒泡

Flags 冒泡就是:每个节点把自己和子孙的 flags 汇总报告给父节点,目的只有一个:让 commit 阶段快速跳过没有变化的子树。

注意⚠️:lanes 冒泡是方便 render 阶段的 beginWork 跳过子树,Flags 冒泡是方便 commit 阶段跳过子树。

<App>              // flags: Passive(有 useEffect)
  <div>            // flags: 0 // 无变化
    <span>1</span> // flags: Update(文本变了)
    <b>hello</b>   // flags: 0 
  </div>
  <Footer />       // flags: 0
</App>


“归”阶段:
complete span   → flags: Update,  subtreeFlags: 0
complete b      → flags: 0,       subtreeFlags: 0

complete div    → flags: 0
                  subtreeFlags: span.flags | b.flags | span.subtreeFlags | b.subtreeFlags
                             = Update      |  0      |  0               |  0
                             = Update
                  ↑ div 自己没变化,但它知道子树里有变化

complete Footerflags: 0,       subtreeFlags: 0

complete Appflags: Passive
                  subtreeFlags: div.flags | Footer.flags | div.subtreeFlags | Footer.subtreeFlags
                             = 0          |  0           |  Update          |  0
                             = Update

那么在 commit 阶段,如何使用这个 flags 呢?流程如下:

commitMutationEffects(App)
  App.subtreeFlags 有 Update → 子树里有活,往下找
  │
  ├── commitMutationEffects(div)
  │     div.subtreeFlags 有 Update → 继续往下
  │     │
  │     ├── commitMutationEffects(span)
  │     │     span.flags 有 Update → 改 DOM!
  │     │     span.subtreeFlags0 → 子树没活,不用往下了
  │     │
  │     └── commitMutationEffects(b)
  │           b.flags0 → 自己没活
  │           b.subtreeFlags0 → 子树没活
  │           直接跳过 ✓
  │
  └── commitMutationEffects(Footer)
        Footer.flags0 → 没活
        Footer.subtreeFlags0 → 子树也没活
        直接跳过 ✓                    ← 整棵 Footer 子树一步跳过

如果没有冒泡的话,则会浪费大量的时间去做无用遍历:

没有冒泡 → commit 阶段必须遍历每一个 fiber 节点检查 flags
  App → div → span → text → b → text → Footer → ...
  大部分节点 flags 是 0,全是无用遍历

有冒泡 → 看一眼 subtreeFlags = 0 就跳过整棵子树
  Footer 及其所有子孙 → 一步跳过

2.5. commit 阶段

commit 阶段分三个子阶段,按顺序同步执行,不可中断:

═══════ Commit 阶段 (同步不可中断) ═══════
    │
    │  commitRoot(root)
    │       │
    │       ├─── Before Mutation 阶段
    │       │      ├─ ClassComponentgetSnapshotBeforeUpdate()
    │       │      └─ 其他 → ...
    │       │
    │       ├─── Mutation 阶段 (操作真实 DOM)
    │       │      对每个 fiber(一遍遍历,利用 subtreeFlags 跳过无关子树):
    │       │      │
    │       │      ├─ ChildDeletion → 处理 fiber.deletions
    │       │      │    ├─ FunctionComponent:
    │       │      │    │    useInsertionEffect destroy()
    │       │      │    │    useLayoutEffect destroy()
    │       │      │    │    useEffect → 收集,等 flushPassiveEffects
    │       │      │    ├─ ClassComponentcomponentWillUnmount()
    │       │      │    └─ HostComponent → removeChild
    │       │      │
    │       │      ├─ Placement → insertBefore / appendChild
    │       │      │
    │       │      └─ Update
    │       │           ├─ FunctionComponent:
    │       │           │    useInsertionEffect destroy → create
    │       │           │    useLayoutEffect destroy(create 等 layout 阶段)
    │       │           ├─ HostComponent → 应用 updateQueue 更新属性
    │       │           └─ HostText → textNode.nodeValue = newText
    │       │
    │       ├─── 切换 current 指针: root.current = finishedWork
    │       │
    │       └─── Layout 阶段
    │              ├─ ClassComponent → componentDidMount / componentDidUpdate
    │              ├─ FunctionComponent → useLayoutEffect create(同步)
    │              └─ Ref 赋值
    │
    ▼
 浏览器绘制 (Paint)
    │
    ▼
 flushPassiveEffects(异步调度,回调同步执行)
    ├─ 第一轮 destroy: 所有需要执行的 useEffect destroy()
    │                  + 被删组件的 useEffect destroy()
    └─ 第二轮 create:  所有需要执行的 useEffect create()

2.5.1. beforeMutation(DOM 操作前)

commitBeforeMutationEffects
│
├─ 遍历 fiber 树(利用 subtreeFlags 跳过无关子树)
│
├─ ClassComponentSnapshot flag):
│    调用 getSnapshotBeforeUpdate(prevProps, prevState)
│    在 DOM 被修改之前拍一张快照,保存一些即将被覆盖的信息。
│
├─ FunctionComponent:
│    无事可做
│
└─ HostRoot:
     清除容器内容(首次渲染时)

2.5.2. mutation (操作 DOM)

Tag 名称           含义
──────────────────────────────────────
HookHasEffect      这个 effect 的 deps 变了,需要执行
HookPassive        这是 useEffect
HookLayout         这是 useLayoutEffect
HookInsertion      这是 useInsertionEffect
commitMutationEffects 遍历 fiber 树:

对每个 fiber:
│
├── ① 检查 ChildDeletion flag → 处理 fiber.deletions 数组
│     forEach deletedFiber:
│       ├─ FunctionComponent:
│       │    ├── effect.tag & HookInsertiondestroy()  同步
│       │    ├── effect.tag & HookLayoutdestroy()  同步
│       │    └── effect.tag & HookPassive → 收集到待清理列表,等 flushPassiveEffects
│       ├─ ClassComponent:
│       │    componentWillUnmount()
│       └─ HostComponent:
│            dom.removeChild(...)
│
├── ② 检查 Placement flag → dom.insertBefore() / dom.appendChild()
│          ├─ HostComponent:
│     		 │    	直接插入 fiber.stateNode
│          └─ FunctionComponent:
│          				自身无 DOM,透明处理:
│          						├── 往上穿透找最近的 HostComponent/HostRoot 作为父 DOM
│                     └── 往下穿透找所有 HostComponent 子节点逐个插入
│
├── ③ 检查 Update flag
│     ├─ FunctionComponent: 遍历 fiber.updateQueueEffect 环形链表):
│     │    ├── effect.tag & HookInsertion & HookHasEffect
│     │    │     → destroy() → create()   destroy + create 一起,在 DOM 操作之前
│     │    │
│     │    └── effect.tag & HookLayout & HookHasEffect
│     │          → destroy() 只清理       create 等 layout 阶段
│     │        useEffect 不在这里处理
│     ├─ HostComponent:
│     │    从 fiber.updateQueue 取出扁平数组 ['key', value, ...]
│     │    逐对应用到 DOM:
│     │    ├── className  → dom.className = value
│     │    ├── style      → Object.assign(dom.style, value)
│     │    ├── children   → dom.textContent = value(纯文本子节点)
│     │    ├── on*        → 更新事件监听器
│     │    ├── value=null → dom.removeAttribute(key)(属性被删)
│     │    └── 其他       → dom.setAttribute(key, value)
│     │
│     ├─ HostText:
│     │     textNode.nodeValue = newText
│     └─ HostText:
│          改 nodeValue
│
└── ④ 检查 Ref flag → 旧 ref 置 null

然后处理下一个 fiber...

2.5.3. 切换 current 树

root.current = finishedWork;
// 就这一行
// 从此刻起,wip 树变成了 current 树
// 下次渲染时它就是"旧的"

这一步在 mutation 之后、layout 之前,所以:

mutation 阶段读 current → 旧树(对的,因为在改 DOM 前/中需要旧信息)

layout 阶段读 current → 新树(对的,componentDidUpdate 看到的是新状态)

2.5.4. layout (DOM 已更新,浏览器还没绘制)

commitLayoutEffects(finishedWork, root)
│
遍历 fiber 树:
│
├─ FunctionComponent:
│    遍历 fiber.updateQueueEffect 链表
│    // 采用位操作:与if (effect.tag & HookLayout && effect.tag & HookHasEffect) {
│      effect.destroy = effect.create()     ← useLayoutEffect 的 create
│    }
│    │
│    └─ 此时 DOM 已更新,可以安全读取 DOM 尺寸/位置
│       如果在 create 里 setState → 同步触发新一轮渲染
│       用户不会看到中间状态
│
├─ ClassComponent:
│    ├─ mount → componentDidMount()
│    └─ update → componentDidUpdate(prevProps, prevState, snapshot)
│
├─ HostComponent:
│    ├─ 如果有 autoFocus 等 → dom.focus()
│    └─ ... 
│ 
└─ 处理 Ref:
     if (flags & Ref) {
       if (typeof ref === 'function') ref(fiber.stateNode)
       else ref.current = fiber.stateNode   ← ref 指向真实 DOM
     }

2.5.5. 调度 useEffect - 浏览器绘制后

commitRoot 末尾:
  if (rootDoesHavePassiveEffects) {
    scheduleCallback(NormalPriority, flushPassiveEffects)
    // 注册异步回调,不在这里执行
  }

────── 浏览器绘制 ──────

flushPassiveEffects()
│
├─ 第一轮:执行 destroy(全部先执行完)
│   │
│   遍历有 Passive flag 的 fiber:
│     遍历 fiber.updateQueue:
│       if (effect.tag & HookPassive && effect.tag & HookHasEffect) {
│         effect.destroy?.()      ← useEffect 上一轮返回的清理函数
│       }
│
└─ 第二轮:执行 create(destroy 全部完成后才开始)
    │
    遍历有 Passive flag 的 fiber:
      遍历 fiber.updateQueueif (effect.tag & HookPassive && effect.tag & HookHasEffect) {
          effect.destroy = effect.create()   ← useEffect 的回调
          // 如果里面有 setState → 触发新一轮更新
        }

3. diff 算法

经典树的 diff 算法,时间复杂度为 O(n*3),react 基于三个假设,将时间复杂度降低为 O(n)

  1. 不同类型的元素产生不同的树:比如 div -> span,直接销毁不再深入比较子树
  2. 同层级比较,不跨层移动:只比较同一父节点下的子节点列表
  3. key 标识节点身份,开发者通过 key 告诉 react 哪一些节点可能被复用

3.1. flags 标记含义与 Commit 阶段的对应

Placement  (0b0000000000000010)  → commitPlacement()  → DOM insertBefore/appendChild
Update     (0b0000000000000100)  → commitUpdate()     → DOM 属性更新
Deletion   (0b0000000000001000)  → commitDeletion()   → DOM removeChild + cleanup

ChildDeletion (0b0000000000010000) → 标记在父节点上,表示有子节点需删除

3.2. diff 的入口与分发

reconcileChildFibers 根据 newChild 的类型进行分发:

reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes)
    │
    ├─ newChild 是 object 且非 null?
    │   ├─ $$typeof === REACT_ELEMENT_TYPE
    │   │   └─▶ reconcileSingleElement()        ← 单个元素
    │   ├─ $$typeof === REACT_PORTAL_TYPE
    │   │   └─▶ reconcileSinglePortal()
    │   └─ isArray(newChild)
    │       └─▶ reconcileChildrenArray()         ← 多个元素(数组)
    │
    ├─ newChild 是 string 或 number?
    │   └─▶ reconcileSingleTextNode()            ← 文本节点
    │
    └─ 以上都不满足?
        └─▶ deleteRemainingChildren()            ← 清空所有旧子节点

3.3. 单节点 diff

当新的 children 只有一个 React 元素时触发,调用reconcileSingleElement,算法流程如下:

reconcileSingleElement(currentFirstChild (旧的第一个子 fiber), element (新的单个 ReactElement))

遍历旧的子 fiber 链表 (child → sibling → sibling → ...)
│
├─ 对每个 oldFiber:
│   │
│   ├─ key 相同?
│   │   ├─ YES → type 也相同?
│   │   │   ├─ YES → ★ 复用该 fiber ★
│   │   │   │        deleteRemainingChildren(剩余兄弟全部标记删除)
│   │   │   │        return useFiber(oldFiber, newProps)
│   │   │   │
│   │   │   └─ NO  → key 对了但 type 变了
│   │   │            deleteRemainingChildren(从当前节点起全部删除)
│   │   │            break → 创建新 fiber
│   │   │
│   │   │   ↑ 为什么 key 相同 type 不同要全部删除?
│   │   │     因为 key 唯一匹配,既然匹配到的 type 不同,
│   │   │     后续兄弟也不可能匹配,全删最优。
│   │   │
│   └─  └─ NO → key 不同
│            deleteChild(仅删除当前 oldFiber)
│            continue → 检查下一个兄弟
│    
│         ↑ 为什么 key 不同只删当前?
│           因为目标 key 可能在后续兄弟中找到。
│   
│
└─ 遍历完没找到可复用的
    └─ createFiberFromElement(element) 创建全新 fiber

具体例子:

示例 1:key 相同,type 相同 → 复用
旧: <div key="a">old</div>
新: <div key="a">new</div>
 
key="a" === "a" ✓  type="div" === "div" ✓
→ 复用 fiber,仅更新 props (children: "old""new")
→ 标记 Update flag

示例 2:key 相同,type 不同 → 全部删除重建
旧: <div key="a"/>, <p key="b"/>, <span key="c"/>
新: <section key="a"/>

遍历旧节点:
  oldFiber[0]: key="a" === "a" ✓, type="div" !== "section" ✗
  → 从此处起全部删除 (div, p, span 全标记 Deletion)
  → 创建新的 <section key="a"/>

示例 3:key 不同 → 逐个删除,继续找
旧: <div key="a"/>, <div key="b"/>, <div key="c"/>
新: <div key="b"/>

遍历旧节点:
  oldFiber[0]: key="a" !== "b" → 删除 <div key="a"/>,继续
  oldFiber[1]: key="b" === "b" ✓, type="div" === "div" ✓
  → 复用!删除剩余兄弟 <div key="c"/>

3.4. 多节点 diff

当新的 children 是数组时触发,调用 reconcileChildrenArray,这是业务中常见的 diff。React 团队发现实际场景中,节点更新的频率远高于增删和移动,因此算法优先处理更新的情况。

两轮遍历

reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes)
│
├─ 第一轮遍历:处理「更新」(最常见情况)
│   从左到右逐个比较key和type,遇到不可复用的就停止
│
├─ 快速路径判断
│   ├─ 新数组遍历完?→ 删除剩余旧节点
│   └─ 旧链表遍历完?→ 剩余新节点全部创建
│
└─ 第二轮遍历:处理「移动/增删」
    构建 Map<key, Fiber>,在剩余旧节点中查找可复用的
    用 lastPlacedIndex 判断是否需要移动
    Map 中剩余未匹配的 → 全部标记 Deletion

第一轮遍历(处理更新)

let oldFiber = currentFirstChild;  // 旧链表头
let newIdx = 0;                     // 新数组下标
let lastPlacedIndex = 0;            // 最后一个不需要移动的旧节点 index

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {

    // 旧 fiber 的 index 大于当前新下标,说明旧链表中有间隙
    if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;    // 不前进旧指针
        oldFiber = null;
    } else {
        nextOldFiber = oldFiber.sibling;
    }

    // 尝试复用: 比较 key 和 type
    const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);

    if (newFiber === null) {
        // key 不同,不可复用 → 跳出第一轮
        break;
    }

    // 如果是新创建的 (oldFiber 存在但 type 不同),标记删除旧的
    if (oldFiber && newFiber.alternate === null) {
        deleteChild(returnFiber, oldFiber);
    }

    // 放置节点,更新 lastPlacedIndex
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

    oldFiber = nextOldFiber;
}

updateSlot 的判定逻辑:

updateSlot 返回 null 意味着 key 不同,第一轮立刻终止。

updateSlot(returnFiber, oldFiber, newChild):
    │
    ├─ newChild 是文本/数字
    │   ├─ oldFiber.key !== nullreturn null (key 不匹配)
    │   └─ oldFiber.key === null → 尝试复用文本节点
    │
    └─ newChild 是 ReactElement
        ├─ newChild.key !== oldFiber.keyreturn null (key 不匹配,跳出循环)
        └─ newChild.key === oldFiber.key
            ├─ type 相同 → useFiber() 复用
            └─ type 不同 → createFiber() 新建

第一轮,具体例子:

情况 A:newIdx === newChildren.length (新数组遍历完了)
┌──────────────────────────────────────────┐
│ 旧: A → B → C → D                       │
│ 新: [A', B']                             │
│                                          │
│ 第一轮: A 复用, B 复用, 新数组遍历完      │
│ → 删除剩余旧节点 C, D                    │
│ → 结束                                   │
└──────────────────────────────────────────┘

情况 B:oldFiber === null (旧链表遍历完了)
┌──────────────────────────────────────────┐
│ 旧: A → B                               │
│ 新: [A', B', C, D]                       │
│                                          │
│ 第一轮: A 复用, B 复用, 旧链表遍历完      │
│ → 为 C, D 创建新 fiber                   │
│ → 标记 Placement                         │
│ → 结束                                   │
└──────────────────────────────────────────┘

第二轮遍历(处理移动)

当第一轮中途跳出(key 不匹配),且新旧都有剩余节点时进入。

Step 1:构建旧节点 Map

// 将剩余旧节点放入 Map<key|index, fiber>
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

// Map 结构:
// key="c" → FiberC
// key="d" → FiberD
// key="e" → FiberE
// (没有 key 的用 index 作为 key)

Step 2:遍历剩余新节点,从 Map 中查找复用

for (; newIdx < newChildren.length; newIdx++) {

    // 从 Map 中查找 key/index 匹配的旧 fiber
    const newFiber = updateFromMap(
        existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes
    );

    if (newFiber !== null) {
        if (newFiber.alternate !== null) {
            // 复用成功,从 Map 中移除(防止重复匹配)
            existingChildren.delete(newFiber.key ?? newIdx);
        }

        // ★ 核心:判断是否需要移动 ★
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    }
}

// Map 中剩余的节点 → 全部标记删除
existingChildren.forEach(child => deleteChild(returnFiber, child));

核心中的核心:placeChild 与 lastPlacedIndex

这是判断节点是否需要移动的算法,理解它就理解了整个多节点 Diff 的精髓。核心在于维护一个 lastPlacedIndex(上一个确认不需要移动的旧节点的 index)。如果当前复用节点在旧序列中的位置比 lastPlacedIndex 靠左,说明它需要移动到右边。

function placeChild(newFiber, lastPlacedIndex, newIndex) {
    newFiber.index = newIndex;

    const current = newFiber.alternate;

    if (current !== null) {
        // 复用的节点:检查是否需要移动
        const oldIndex = current.index;

        if (oldIndex < lastPlacedIndex) {
            // ★ 旧位置在「最后不动锚点」左边 → 需要移动到右边
            newFiber.flags |= Placement;
            return lastPlacedIndex;  // 锚点不变
        } else {
            // 旧位置在锚点右边或等于 → 不需要移动
            return oldIndex;         // 更新锚点为当前旧位置
        }
    } else {
        // 全新节点:标记插入
        newFiber.flags |= Placement;
        return lastPlacedIndex;
    }
}

第二轮,具体例子:

旧: A(0) → B(1) → C(2) → D(3)     (括号内是 oldIndex)
新: [A,    C,     D,     B]

═══ 第一轮遍历 ═══
i=0: new=A, old=A → key 相同, type 相同 → 复用
     placeChild: oldIndex=0, lastPlacedIndex=0
                 0 >= 0 → 不移动, lastPlacedIndex=0

i=1: new=C, old=B → key 不同 → break!

═══ 第二轮遍历 ═══
构建 Map: { "B"Fiber(1), "C"Fiber(2), "D"Fiber(3) }

i=1: new=C → Map 找到 Fiber(2)
     placeChild: oldIndex=2, lastPlacedIndex=0
                 2 >= 0 → 不移动, lastPlacedIndex=2 ✅

i=2: new=D → Map 找到 Fiber(3)
     placeChild: oldIndex=3, lastPlacedIndex=2
                 3 >= 2 → 不移动, lastPlacedIndex=3 ✅

i=3: new=B → Map 找到 Fiber(1)
     placeChild: oldIndex=1, lastPlacedIndex=3
                 1 < 3  → ★ 需要移动! ★ 标记 Placement
                 lastPlacedIndex 保持 3

结果: A 不动, C 不动, D 不动, B 移动到末尾
DOM操作: 仅移动 B → 最少操作 ✓
旧: A(0) → B(1) → C(2) → D(3)
新: [D,    A,     B,     C]

═══ 第一轮遍历 ═══
i=0: new=D, old=A → key 不同 → break!

═══ 第二轮遍历 ═══
构建 Map: { "A"Fiber(0), "B"Fiber(1), "C"Fiber(2), "D"Fiber(3) }

i=0: new=D → Map 找到 Fiber(3)
     placeChild: oldIndex=3, lastPlacedIndex=0
                 3 >= 0 → 不移动, lastPlacedIndex=3

i=1: new=A → Map 找到 Fiber(0)
     placeChild: oldIndex=0, lastPlacedIndex=3
                 0 < 3  → ★ 移动! ★

i=2: new=B → Map 找到 Fiber(1)
     placeChild: oldIndex=1, lastPlacedIndex=3
                 1 < 3  → ★ 移动! ★

i=3: new=C → Map 找到 Fiber(2)
     placeChild: oldIndex=2, lastPlacedIndex=3
                 2 < 3  → ★ 移动! ★

结果: D 不动, A/B/C 都标记移动
DOM操作: 3 次移动 (最坏情况)

实际上最优解只需移动 D 到头部 (1 次)
→ 这就是 React Diff 的局限性: 右移友好,左移代价高
旧: A(0) → B(1) → C(2) → D(3) → E(4)
新: [A,    C,     F,     B]

═══ 第一轮遍历 ═══
i=0: new=A, old=A → 复用, lastPlacedIndex=0

i=1: new=C, old=B → key 不同 → break!

═══ 第二轮遍历 ═══
Map: { "B"Fiber(1), "C"Fiber(2), "D"Fiber(3), "E"Fiber(4) }

i=1: new=C → Map 找到 Fiber(2)
     oldIndex=2 >= lastPlacedIndex=0 → 不动, lastPlacedIndex=2

i=2: new=F → Map 中没有 key="F"
     → 新建 fiber, 标记 Placement

i=3: new=B → Map 找到 Fiber(1)
     oldIndex=1 < lastPlacedIndex=2 → ★ 移动!

Map 剩余: { "D"Fiber(3), "E"Fiber(4) }
→ D, E 标记 Deletion

最终 DOM 操作:
  - A: 不动 (Update props if needed)
  - C: 不动
  - F: 插入 (Placement)
  - B: 移动到末尾 (Placement)
  - D: 删除 (Deletion)
  - E: 删除 (Deletion)

4. hooks 实现

4.1. useState

基于上述的工作原理,那么我们可以试着实现一个 useState 的最小实现 demo(ps:只是一个小 demo,只实现核心的流程,后续 react版的完整实现,会在下一篇文章产出)

// action 可以是新值,也可以是 updater 函数
// 例如:setCount(1) 或 setCount(prev => prev + 1)

// Hook 节点:挂在 Fiber.memoizedState 链表上
function createHook() {
  return {
    memoizedState: null, // 当前 hook 对应的状态
    queue: null, // 指向最后一个 update,形成环状链表
    next: null, // 下一个 hook
  }
}

// Fiber:这里只保留最小字段
function createFiber() {
  return {
    memoizedState: null, // hook 链表头
    alternate: null, // 指向上一次渲染的 Fiber
  }
}

// 当前正在渲染的 Fiber
let currentlyRenderingFiber = null

// 新 Fiber 上,当前正在构建的 hook
let workInProgressHook = null

// 旧 Fiber 上,当前正在读取的 hook
let currentHook = null

// 根 Fiber
let rootFiber = null

// 根组件和 props,方便 setState 后重新渲染
let rootComponent = null
let rootProps = null

// 是否正在渲染中
let isRendering = false

// 渲染阶段里如果触发了 setState,就在本轮结束后补跑一次
let didScheduleRenderPhaseUpdate = false

function resolveStateAction(prevState, action) {
  return typeof action === "function" ? action(prevState) : action
}

function resolveInitialState(initialState) {
  return typeof initialState === "function" ? initialState() : initialState
}

// 创建一个 update 节点
function createUpdate(action) {
  return {
    action,
    next: null,
  }
}

// 把 update 放进环状链表
function enqueueUpdate(queue, update) {
  // queue.pending 指向最后一个 update
  if (queue.pending === null) {
    // 第一个 update,自环
    update.next = update
  } else {
    // 插入到尾部后面
    update.next = queue.pending.next
    queue.pending.next = update
  }

  // pending 永远指向最后一个
  queue.pending = update
}

// 消费 queue,依次算出最新 state
function processUpdateQueue(baseState, queue) {
  const pending = queue.pending

  if (pending === null) {
    return baseState
  }

  let newState = baseState
  const first = pending.next
  let update = first

  do {
    newState = resolveStateAction(newState, update.action)
    update = update.next
  } while (update !== first)

  // 本轮消费完,清空队列
  queue.pending = null
  return newState
}

// 开始渲染 hooks
function prepareToUseHooks(fiber) {
  currentlyRenderingFiber = fiber
  workInProgressHook = null
  currentHook = fiber.alternate ? fiber.alternate.memoizedState : null

  // 每次渲染都会重新构建当前 Fiber 的 hook 链表
  fiber.memoizedState = null
}

// 结束渲染 hooks
function finishHooks() {
  currentlyRenderingFiber = null
  workInProgressHook = null
  currentHook = null
}

// 把 hook 挂到当前 Fiber 的链表尾部
function pushHook(hook) {
  if (currentlyRenderingFiber === null) {
    throw new Error("Hooks can only be called inside a function component.")
  }

  if (workInProgressHook === null) {
    // 第一个 hook,挂到 fiber.memoizedState
    currentlyRenderingFiber.memoizedState = hook
  } else {
    // 后续 hook 挂到前一个 hook 的 next
    workInProgressHook.next = hook
  }

  workInProgressHook = hook
  return hook
}

// 调度重新渲染
function scheduleRender() {
  if (rootComponent === null) {
    return
  }

  // 如果正在 render 中,不要重入 render
  // 只做标记,等当前 render 结束后再补跑
  if (isRendering) {
    didScheduleRenderPhaseUpdate = true
    return
  }

  render(rootComponent, rootProps)
}

function useState(initialState) {
  if (currentlyRenderingFiber === null) {
    throw new Error("useState must be called during render.")
  }

  let hook

  // update 阶段:从旧 hook 复制并计算新状态
  if (currentHook !== null) {
    const oldHook = currentHook
    const queue = oldHook.queue
    const newState = processUpdateQueue(oldHook.memoizedState, queue)

    hook = createHook()
    hook.memoizedState = newState
    hook.queue = queue

    pushHook(hook)

    // currentHook 往后挪,供下一个 useState 使用
    currentHook = oldHook.next
  } else {
    // mount 阶段:创建新 hook
    hook = createHook()
    hook.memoizedState = resolveInitialState(initialState)
    hook.queue = { pending: null }

    pushHook(hook)
  }

  // dispatch 永远往这个 hook 的 queue 里加更新
  function dispatch(action) {
    const update = createUpdate(action)
    enqueueUpdate(hook.queue, update)
    scheduleRender()
  }

  return [hook.memoizedState, dispatch]
}

function render(component, props) {
  rootComponent = component
  rootProps = props || {}

  let result

  do {
    didScheduleRenderPhaseUpdate = false
    isRendering = true

    // 新建本轮的 workInProgress Fiber
    const workInProgress = createFiber()

    // alternate 指向旧 Fiber
    workInProgress.alternate = rootFiber

    prepareToUseHooks(workInProgress)

    try {
      // 执行函数组件,期间会调用 useState
      result = component(rootProps)
    } finally {
      finishHooks()
      isRendering = false
    }

    // 本轮渲染完成,把新 Fiber 变成当前根
    rootFiber = workInProgress

    // 如果渲染期间有人 setState,则继续补跑一轮
  } while (didScheduleRenderPhaseUpdate)

  return result
}

再给出一个运行例子

/**
 * demo 代码
 */

function Counter() {
  // 只保留一个 state,方便观察更新流程
  const [count, setCount] = useState(0);

  // render 阶段只读取 state
  console.log("render ->", count);

  // 返回事件处理函数,模拟 React 里的 onClick
  return {
    increment() {
      setCount((prev) => prev + 1);
    },
  };
}

// 首次渲染
const app = render(Counter);

// 模拟用户点击按钮
app.increment(); // 0 -> 1
app.increment(); // 1 -> 2
用户点击 increment
  |
  v
dispatch(action)
  |
  |-- createUpdate(action)
  |-- enqueueUpdate(queue, update)
  |
  v
scheduleRender()
  |
  v
render(Counter)
  |
  |================ 当前这一轮的新 Fiber ================
  |
  |   new Fiber
  |     |
  |     |-- alternate ----------------------.
  |                                          \
  |                                           \
  |================ 上一轮的旧 Fiber ========== \ =========
  |                                              \
  |   old Fiber                                   \
  |     |                                          \
  |     |-- Hook(state: 0, queue: update...) <------'
  |
  v
执行 Counter()
  |
  |-- useState(0)
        |
        |-- 读取 old Hook
        |-- processUpdateQueue(baseState, queue)
        |-- 算出 newState
        |-- 创建 new Hook(state: 1)
        |-- 挂到 new Fiber.memoizedState
  |
  v
render -> 1
  |
  v
rootFiber = new Fiber

4.2. useEffect

那么我们也可以试着实现一个 useEffect 的最小实现 demo(ps:同样也只是一个小 demo,后续 react版完整实现,同样也会在下一篇文章产出)

// Hook 节点:挂在 Fiber.memoizedState 链表上
function createHook() {
  return {
    memoizedState: null, // useState: state;useEffect: { deps, cleanup }
    queue: null, // useState 用的更新队列
    next: null, // 下一个 hook
  };
}

// Fiber:这里只保留最小字段
function createFiber() {
  return {
    memoizedState: null, // hook 链表头
    alternate: null, // 指向上一次渲染的 Fiber
    effects: [], // 本轮 render 收集到的 effect
  };
}

// 当前正在渲染的 Fiber
let currentlyRenderingFiber = null;

// 新 Fiber 上,当前正在构建的 hook
let workInProgressHook = null;

// 旧 Fiber 上,当前正在读取的 hook
let currentHook = null;

// 根 Fiber
let rootFiber = null;

// 根组件和 props,方便 setState 后重新渲染
let rootComponent = null;
let rootProps = null;

// 是否正在渲染中
let isRendering = false;

// 渲染阶段里如果触发了 setState,就在本轮结束后补跑一次
let didScheduleRenderPhaseUpdate = false;

function resolveStateAction(prevState, action) {
  return typeof action === "function" ? action(prevState) : action;
}

function resolveInitialState(initialState) {
  return typeof initialState === "function" ? initialState() : initialState;
}

// 创建一个 update 节点
function createUpdate(action) {
  return {
    action,
    next: null,
  };
}

// 把 update 放进环状链表
function enqueueUpdate(queue, update) {
  if (queue.pending === null) {
    update.next = update;
  } else {
    update.next = queue.pending.next;
    queue.pending.next = update;
  }

  queue.pending = update;
}

// 消费 queue,依次算出最新 state
function processUpdateQueue(baseState, queue) {
  const pending = queue.pending;

  if (pending === null) {
    return baseState;
  }

  let newState = baseState;
  const first = pending.next;
  let update = first;

  do {
    newState = resolveStateAction(newState, update.action);
    update = update.next;
  } while (update !== first);

  queue.pending = null;
  return newState;
}

// 开始渲染 hooks
function prepareToUseHooks(fiber) {
  currentlyRenderingFiber = fiber;
  workInProgressHook = null;
  currentHook = fiber.alternate ? fiber.alternate.memoizedState : null;
  fiber.memoizedState = null;
  fiber.effects = [];
}

// 结束渲染 hooks
function finishHooks() {
  currentlyRenderingFiber = null;
  workInProgressHook = null;
  currentHook = null;
}

// 把 hook 挂到当前 Fiber 的链表尾部
function pushHook(hook) {
  if (currentlyRenderingFiber === null) {
    throw new Error("Hooks can only be called inside a function component.");
  }

  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = hook;
  } else {
    workInProgressHook.next = hook;
  }

  workInProgressHook = hook;
  return hook;
}

// 调度重新渲染
function scheduleRender() {
  if (rootComponent === null) {
    return;
  }

  if (isRendering) {
    didScheduleRenderPhaseUpdate = true;
    return;
  }

  render(rootComponent, rootProps);
}

// 极简版 useState
function useState(initialState) {
  if (currentlyRenderingFiber === null) {
    throw new Error("useState must be called during render.");
  }

  let hook;

  if (currentHook !== null) {
    const oldHook = currentHook;
    const queue = oldHook.queue;
    const newState = processUpdateQueue(oldHook.memoizedState, queue);

    hook = createHook();
    hook.memoizedState = newState;
    hook.queue = queue;

    pushHook(hook);
    currentHook = oldHook.next;
  } else {
    hook = createHook();
    hook.memoizedState = resolveInitialState(initialState);
    hook.queue = { pending: null };

    pushHook(hook);
  }

  function dispatch(action) {
    const update = createUpdate(action);
    enqueueUpdate(hook.queue, update);
    scheduleRender();
  }

  return [hook.memoizedState, dispatch];
}

// 比较两次依赖数组是否完全相等
// React 里也是逐项用 Object.is 比较
function areHookInputsEqual(nextDeps, prevDeps) {
  // 只要有一边是 null,就认为不能复用
  // 这里的 null 表示“没有传 deps”,这种情况应该每次都执行
  if (prevDeps === null || nextDeps === null) {
    return false;
  }

  // 长度不同,直接认为依赖变化
  if (nextDeps.length !== prevDeps.length) {
    return false;
  }

  // 逐项比较
  for (let i = 0; i < nextDeps.length; i += 1) {
    if (!Object.is(nextDeps[i], prevDeps[i])) {
      return false;
    }
  }

  return true;
}

// 统一执行本轮 render 收集到的 effects
function flushEffects(effects) {
  for (const effect of effects) {
    // 如果上一次 effect 返回过 cleanup,
    // 并且这次依赖发生变化了,
    // 那么先执行旧 cleanup
    if (typeof effect.prevCleanup === "function") {
      effect.prevCleanup();
    }

    // 再执行新的 effect 回调
    // effect 回调可以选择返回 cleanup 函数
    const cleanup = effect.create();

    // 把新的 cleanup 保存回当前 hook 上
    // 供下次依赖变化时执行
    effect.hook.memoizedState.cleanup =
      typeof cleanup === "function" ? cleanup : null;
  }
}

// 极简版 useEffect
function useEffect(create, deps) {
  // 和 useState 一样,只能在函数组件 render 阶段调用
  if (currentlyRenderingFiber === null) {
    throw new Error("useEffect must be called during render.");
  }

  // 不传 deps 时,表示每次 render 后都执行
  // 这里统一用 null 表示“无依赖数组”
  const nextDeps = deps === undefined ? null : deps;

  let hook;

  // update 阶段:当前有旧 hook 可以复用
  if (currentHook !== null) {
    const oldHook = currentHook;

    // oldHook.memoizedState 里保存的是上一次 effect 的信息
    // 结构是:{ deps, cleanup }
    const prevEffect = oldHook.memoizedState;

    // 为本轮创建一个新的 hook 节点
    hook = createHook();

    // 先把本轮 deps 记下来
    // cleanup 先沿用旧的,等 flushEffects 真正执行完新的 effect 后再覆盖
    hook.memoizedState = {
      deps: nextDeps,
      cleanup: prevEffect.cleanup,
    };

    // 挂到当前 Fiber 的 hook 链表上
    pushHook(hook);

    // 旧 hook 指针后移,供下一个 hook 使用
    currentHook = oldHook.next;

    // 判断这次 effect 是否需要执行
    const shouldRun =
      // 没传 deps:每次都执行
      nextDeps === null ||
      // 上一次没传 deps:也认为需要执行
      prevEffect.deps === null ||
      // 依赖数组不同:需要执行
      !areHookInputsEqual(nextDeps, prevEffect.deps);

    // 只有依赖变化了,才把 effect 放入本轮待执行列表
    if (shouldRun) {
      currentlyRenderingFiber.effects.push({
        create, // 新的 effect 回调
        hook, // 当前新 hook,执行后要把 cleanup 写回这里
        prevCleanup: prevEffect.cleanup, // 上一次的 cleanup,执行新 effect 前先清理
      });
    }
  } else {
    // mount 阶段:首次渲染,没有旧 hook
    hook = createHook();

    // 首次挂载时先记录 deps,cleanup 还没有
    hook.memoizedState = {
      deps: nextDeps,
      cleanup: null,
    };

    // 挂到当前 Fiber 的 hook 链表
    pushHook(hook);

    // 首次挂载一定要执行一次 effect
    currentlyRenderingFiber.effects.push({
      create,
      hook,
      prevCleanup: null,
    });
  }
}

function render(component, props) {
  rootComponent = component;
  rootProps = props || {};

  let result;

  do {
    didScheduleRenderPhaseUpdate = false;
    isRendering = true;

    // 创建本轮 workInProgress Fiber
    const workInProgress = createFiber();

    // alternate 指向上一次渲染结果
    workInProgress.alternate = rootFiber;

    // 准备开始读取 / 构建 hooks
    prepareToUseHooks(workInProgress);

    try {
      // 执行组件函数
      // 期间 useState / useEffect 会依次挂到 Fiber.memoizedState 链表上
      result = component(rootProps);
    } finally {
      finishHooks();
      isRendering = false;
    }

    // 本轮 render 完成后,当前 Fiber 成为新的 rootFiber
    rootFiber = workInProgress;

    // 如果 render 过程中触发了 setState,就补跑下一轮 render
  } while (didScheduleRenderPhaseUpdate);

  // 重点:
  // 只有 render 全部稳定结束后,才统一执行本轮收集到的 effect
  const effects = rootFiber.effects.slice();
  rootFiber.effects.length = 0;
  flushEffects(effects);

  return result;
}

再给一个运行例子

function Counter() {
  // 只保留一个 state,方便观察 state 和 effect 的关系
  const [count, setCount] = useState(0);

  console.log("render ->", count);

  // count 变化后,render 结束再执行 effect
  useEffect(() => {
    console.log("effect: count changed ->", count);

    return () => {
      console.log("cleanup: previous count ->", count);
    };
  }, [count]);

  return {
    increment() {
      setCount((prev) => prev + 1);
    },
  };
}

const app = render(Counter);
app.increment();
app.increment();
render(Counter)
  |
  |================ 当前这一轮的新 Fiber ================
  |
  |   new Fiber
  |     |
  |     |-- memoizedState
  |     |     |
  |     |     -> Hook1(state: count)
  |     |           |
  |     |           -> Hook2(effect: { deps, cleanup })
  |     |
  |     -> effects[]
  |
  v
执行 Counter()
  |
  |-- useState(0)
  |     |
  |     -> 读/写 state hook
  |
  |-- useEffect(create, [count])
        |
        |-- 读取 old Hook2 的 deps/cleanup
        |-- 比较 old deps 和 new deps
        |
        |-- 如果 deps 变了:
        |     把 effect 放进 new Fiber.effects
        |
        |-- 注意:这里还不执行 create
  |
  v
render 结束
  |
  v
flushEffects(new Fiber.effects)
  |
  |-- 先执行 prevCleanup()
  |-- 再执行 create()
  |-- 拿到新的 cleanup
  |-- 保存到 Hook2.memoizedState.cleanup
  |
  v
下一轮更新继续复用