TOP

React Reconciliation 协调核心过程

Reconciliation(协调/调和)是 React 的 diff 算法核心,它决定了”哪些节点需要创建/更新/删除”。

React 通过对比新旧 Fiber 树,标记需要更新的节点,然后一次性提交到 DOM。这个过程是可中断的、可恢复的,支持并发渲染。

核心思想:React 用循环(而不是递归)处理工作单元,通过处理 effectList 和 lanes 调度优先级任务。

什么是 Reconciliation?

设想一个场景:你点击按钮,状态从 count: 1 变成 count: 2,React 需要做什么?

  1. 对比新旧虚拟 DOM:找出哪些节点变了
  2. 标记更新类型:创建(Placement)、更新(Update)、删除(Deletion)
  3. 构建新的 Fiber 树:在内存中构建,不直接改 DOM
  4. 提交到 DOM:一次性应用所有变更

这就是 Reconciliation 的全过程。

Double Buffering 双缓冲机制

React 使用双缓冲技术,同时维护两棵 Fiber 树:

树类型作用生命周期
current当前已渲染的树稳定,对应真实 DOM
workInProgress正在构建的新树临时,构建完成后替换 current
// 双缓冲的核心逻辑
let current = root.current;  // 当前树
let workInProgress = createWorkInProgress(current, pendingProps);  // 新树

// 构建完成后
root.current = workInProgress;  // 新树变成当前树

为什么需要双缓冲?

  1. 可中断渲染:可以随时丢弃 workInProgress,回退到 current
  2. 并发安全:current 树始终稳定,不会在渲染中被破坏
  3. 性能优化:复用 Fiber 节点,减少内存分配

Effect 标记:Placement / Update / Deletion

React 通过 effect 标记告诉 Commit 阶段”要做什么”:

// Effect 类型(位掩码)
const Placement = 0b00000000000000010;      // 插入新节点
const Update = 0b00000000000000100;         // 更新属性
const Deletion = 0b00000000000001000;       // 删除节点
const ContentReset = 0b00000000000010000;   // 重置内容
const Ref = 0b00000000000100000;            // 更新 ref
const Passive = 0b00000000010000000;        // useEffect 副作用

标记过程

function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
  lanes: Lanes
): Fiber | null {
  // 1. 单节点对比
  if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(
          reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes)
        );
    }
  }
  
  // 2. 多节点对比(数组)
  if (isArray(newChild)) {
    return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
  }
  
  // 3. 删除多余节点
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

标记示例

// 旧树
<div>
  <span key="a">A</span>
  <span key="b">B</span>
</div>

// 新树
<div>
  <span key="b">B</span>
  <span key="c">C</span>
</div>

标记结果:

Key 如何影响 Reconciliation

Key 是 React 识别节点的唯一标识,直接影响 diff 性能。

没有 Key 的情况

// 旧:A, B, C
// 新:B, C, D

// React 的对比策略(按索引)
// 索引 0: A → B (Update)
// 索引 1: B → C (Update)
// 索引 2: C → D (Update)
// 结果:3 次更新,性能差

有 Key 的情况

// 旧:<div key="a">A</div>, <div key="b">B</div>, <div key="c">C</div>
// 新:<div key="b">B</div>, <div key="c">C</div>, <div key="d">D</div>

// React 的对比策略(按 key)
// key="a" → 删除
// key="b" → 复用(移动位置)
// key="c" → 复用(移动位置)
// key="d" → 插入
// 结果:1 删除 + 2 移动 + 1 插入,性能好

Key 的匹配算法

function reconcileChildrenArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildren: Array<any>,
  lanes: Lanes
): Fiber | null {
  // 1. 第一次遍历:从左到右找相同 key
  let resultingFirstChild: Fiber | null = null;
  let previousNewFiber: Fiber | null = null;
  
  let oldFiber = currentFirstChild;
  let newIdx = 0;
  let nextOldFiber = null;
  
  // 2. 遍历新数组,在旧数组中找匹配
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    if (oldFiber.index > newIdx) {
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      nextOldFiber = oldFiber.sibling;
    }
    
    const newFiber = updateSlot(
      returnFiber,
      oldFiber,
      newChildren[newIdx],
      lanes
    );
    
    if (newFiber === null) {
      if (oldFiber === null) {
        oldFiber = nextOldFiber;
      }
      break;  // key 不匹配,跳出
    }
    
    // 标记更新或复用
    if (shouldTrackSideEffects) {
      if (oldFiber && newFiber.alternate === null) {
        deleteChild(returnFiber, oldFiber);  // 标记删除
      }
    }
    
    // 构建链表
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }
  
  // 3. 第二次遍历:处理剩余节点
  if (newIdx === newChildren.length) {
    // 新数组遍历完,删除剩余的旧节点
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }
  
  if (oldFiber === null) {
    // 旧数组遍历完,插入剩余的新节点
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      if (newFiber === null) continue;
      // ... 插入逻辑
    }
    return resultingFirstChild;
  }
  
  // 4. 构建 key 映射表,处理乱序情况
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
  
  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx],
      lanes
    );
    // ... 复用或创建逻辑
  }
  
  // 删除未使用的旧节点
  if (shouldTrackSideEffects) {
    existingChildren.forEach(child => deleteChild(returnFiber, child));
  }
  
  return resultingFirstChild;
}

为什么 key 不能是 index?

使用 index 作为 key 会导致 React 错误地复用节点,造成状态混乱和性能问题。

问题示例:列表删除场景

// 初始状态
const [items, setItems] = useState([
  { id: 1, name: 'Apple' },
  { id: 2, name: 'Banana' },
  { id: 3, name: 'Cherry' }
]);

// 渲染(错误:使用 index 作为 key)
{items.map((item, index) => (
  <TodoItem key={index} item={item} />
))}

// 删除第一个元素后
setItems([
  { id: 2, name: 'Banana' },
  { id: 3, name: 'Cherry' }
]);

使用 index 作为 key 的问题

删除前:
索引 0: <TodoItem key={0} item={Apple} /> 有输入框,用户输入了 "test"
索引 1: <TodoItem key={1} item={Banana} />
索引 2: <TodoItem key={2} item={Cherry} />

删除后(React 的对比):
索引 0: <TodoItem key={0} item={Banana} /> React 认为这是同一个节点!
索引 1: <TodoItem key={1} item={Cherry} />

结果:
- React 复用索引 0 Fiber 节点
- 但数据变成了 Banana
- 输入框的状态("test")被保留,但显示的是 Banana
- 用户看到 Banana 的输入框里有 "test"(错误!)

使用稳定的 key

// 正确:使用 item.id 作为 key
{items.map((item) => (
  <TodoItem key={item.id} item={item} />
))}
删除前:
key=1: <TodoItem key={1} item={Apple} />
key=2: <TodoItem key={2} item={Banana} />
key=3: <TodoItem key={3} item={Cherry} />

删除后(React 的对比):
key=1: 删除 
key=2: <TodoItem key={2} item={Banana} />   ← 正确复用
key=3: <TodoItem key={3} item={Cherry} /> 正确复用

结果:
- React 正确识别哪些节点被删除
- 状态正确对应到正确的节点
- 用户体验正常 

核心问题

  1. 状态错位:index 变化后,React 会复用错误的 Fiber 节点,导致状态错位
  2. 性能问题:无法正确复用节点,导致不必要的更新和重新渲染
  3. 副作用混乱:useEffect、useLayoutEffect 等副作用可能绑定到错误的节点

getElementKey:提取 key 的机制

React 通过 getElementKey 函数从 React 元素中提取 key:

function getElementKey(element: ReactElement, index: number): string | number {
  // 1. 如果元素有显式的 key,使用它
  if (element.key !== null && element.key !== undefined) {
    return element.key;
  }
  
  // 2. 如果没有 key,使用索引(不推荐,但 React 会这样做)
  return index.toString();
}

提取过程

// 组件渲染
function TodoList({ items }) {
  return (
    <div>
      {items.map((item, index) => (
        <TodoItem key={item.id} item={item} />
      ))}
    </div>
  );
}

// React 内部处理
function reconcileChildrenArray(...) {
  for (let newIdx = 0; newIdx < newChildren.length; newIdx++) {
    const newChild = newChildren[newIdx];
    
    // 提取 key
    const elementKey = getElementKey(newChild, newIdx);
    // 如果 newChild.key = item.id,则 elementKey = item.id
    // 如果 newChild.key = null,则 elementKey = newIdx.toString()
    
    // 使用 key 匹配旧节点
    const oldFiber = findOldFiberByKey(elementKey);
    // ...
  }
}

getElementKey 的作用

  1. 统一 key 提取:无论 key 是字符串、数字还是其他类型,统一处理
  2. 降级处理:如果没有显式 key,使用索引作为降级方案
  3. 性能优化:通过 key 快速匹配新旧节点,避免遍历查找

Key 的最佳实践

场景推荐不推荐
列表渲染key={item.id}key={index}
动态列表稳定的唯一标识随机数、时间戳
固定列表可省略(React 会用索引)-

beginWork:向下遍历构建新树

beginWork 是 Render Phase 的核心,负责处理单个 Fiber 节点:

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  // 1. 检查优先级:如果当前节点优先级不够,跳过
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    
    if (oldProps !== newProps || hasLegacyContextChanged()) {
      didReceiveUpdate = true;
    } else {
      // 可以复用,检查子节点
      const didBailout = attemptToOptimizeUpdate(current, workInProgress, renderLanes);
      if (didBailout) {
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
      }
    }
  } else {
    didReceiveUpdate = false;
  }
  
  // 2. 根据组件类型处理
  switch (workInProgress.tag) {
    case FunctionComponent:
      return updateFunctionComponent(current, workInProgress, Component, newProps, renderLanes);
    case ClassComponent:
      return updateClassComponent(current, workInProgress, Component, newProps, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);
    // ... 其他类型
  }
}

处理函数组件

function updateFunctionComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes
) {
  // 1. 重置 Hooks
  prepareToUseHooks(current, workInProgress);
  
  // 2. 执行组件函数,得到 children
  let nextChildren = renderWithHooks(
    current,
    workInProgress,
    Component,
    nextProps,
    context,
    renderLanes
  );
  
  // 3. 对比 children,标记 effect
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  
  return workInProgress.child;  // 返回第一个子节点
}

处理 Host 组件(DOM 节点)

function updateHostComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
) {
  const nextProps = workInProgress.pendingProps;
  const nextChildren = nextProps.children;
  
  // 标记属性更新
  markRef(current, workInProgress);
  
  // 对比 children
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  
  return workInProgress.child;
}

completeUnitOfWork:向上回溯收集 Effect

completeUnitOfWork 在完成一个节点后,向上回溯收集副作用:

function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
  workInProgress = unitOfWork;
  
  do {
    const current = workInProgress.alternate;
    const returnFiber = workInProgress.return;
    
    // 1. 完成当前节点的工作
    if ((workInProgress.flags & Incomplete) === NoFlags) {
      let next = completeWork(current, workInProgress, subtreeRenderLanes);
      
      if (next !== null) {
        // 还有工作要做,返回继续处理
        return next;
      }
      
      // 2. 收集 effect 到父节点
      if (
        returnFiber !== null &&
        (returnFiber.flags & Incomplete) === NoFlags
      ) {
        // 把当前节点的 effect 链到父节点的 effectList
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = workInProgress.firstEffect;
        }
        if (workInProgress.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
          }
          returnFiber.lastEffect = workInProgress.lastEffect;
        }
        
        // 如果当前节点有 effect,也链上去
        const flags = workInProgress.flags;
        if (flags > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress;
          } else {
            returnFiber.firstEffect = workInProgress;
          }
          returnFiber.lastEffect = workInProgress;
        }
      }
    } else {
      // 节点未完成(被中断),标记父节点
      if (returnFiber !== null) {
        returnFiber.flags |= Incomplete;
        returnFiber.subtreeFlags = NoFlags;
        returnFiber.deletions = null;
      }
    }
    
    // 3. 处理兄弟节点
    const siblingFiber = workInProgress.sibling;
    if (siblingFiber !== null) {
      return siblingFiber;  // 返回兄弟节点继续处理
    }
    
    // 4. 没有兄弟节点,向上回溯
    workInProgress = returnFiber;
  } while (workInProgress !== null);
  
  // 5. 回到根节点,工作完成
  return null;
}

EffectList 的构建过程

处理节点 A

completeWork(A)
  ├─ 标记 A effect
  └─ A effect 链到父节点

处理节点 B(A 的兄弟)

completeWork(B)
  ├─ 标记 B effect
  └─ B effect 链到父节点(接在 A 后面)

回溯到父节点
  ├─ firstEffect A
  └─ lastEffect B

循环遍历:workLoop 的核心

React 使用循环而非递归遍历 Fiber 树,支持中断和恢复:

function workLoopConcurrent() {
  // 循环处理工作单元
  while (workInProgress !== null && !shouldYield()) {
    // 1. 向下遍历:beginWork
    workInProgress = performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
  // 向下遍历
  let next = beginWork(current, unitOfWork, subtreeRenderLanes);
  
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  
  if (next === null) {
    // 没有子节点,向上回溯
    next = completeUnitOfWork(unitOfWork);
  }
  
  return next;
}

遍历流程

遍历流程

中断与恢复

React 的循环遍历支持中断和恢复,这是实现时间分片和优先级调度的基础。当时间片用完或有更高优先级的任务时,React 会中断当前工作,让出控制权给浏览器。恢复时,React 可以从中断的地方精确继续。

function workLoopConcurrent() {
  // 循环处理工作单元,支持中断
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
  // 中断时,workInProgress 指针保存了当前正在处理的节点
  // 恢复时,可以从这个节点继续处理
}

关于中断与恢复的详细机制,包括状态保存、优先级匹配处理、bailout 机制等,请参考:React 中断与恢复

总结

Reconciliation 是 React 性能的核心,通过以下机制实现高效更新:

  1. 双缓冲currentworkInProgress 两棵树,支持可中断渲染
  2. Effect 标记Placement/Update/Deletion,精确描述变更
  3. Key 优化:通过 key 匹配复用节点,减少不必要的创建和删除
  4. 循环遍历beginWork 向下,completeUnitOfWork 向上,支持中断恢复
  5. 优先级调度:通过 lanes 控制哪些更新优先处理

整个过程在内存中完成,不直接操作 DOM,最后在 Commit 阶段一次性提交,保证性能与一致性。