Skip to content

第6章 协调过程

本章将深入React的协调过程(Reconciliation),理解render阶段的工作原理、beginWork和completeWork的实现、以及著名的Diff算法。这是React更新机制的核心。

在前面的章节中,我们学习了Fiber架构和调度系统。Fiber为React提供了可中断的工作单元,Scheduler决定了何时执行这些工作。但是,React如何知道需要做哪些工作?如何高效地找出Virtual DOM的变化?如何将这些变化应用到真实DOM?

这就是协调过程(Reconciliation)的职责。协调过程是React的核心算法,它负责比较新旧Fiber树,找出差异,并标记需要执行的DOM操作。

为什么React需要协调过程?直接重新渲染整个页面不行吗?Diff算法是如何工作的?key属性为什么如此重要?beginWork和completeWork分别做了什么?

本章将逐一解答这些问题,带你深入理解React协调过程的设计与实现。


6.1 render阶段概述

render阶段是React更新流程的第一个阶段,它负责构建新的Fiber树,并标记需要执行的副作用。

6.1.1 performSyncWorkOnRoot

当有同步更新时,React会调用performSyncWorkOnRoot函数:

javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
// 行号:1200-1250(React 19.3.0)

function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes) {
  // 同步渲染的入口
  
  // 1. 准备工作
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext;
  
  // 2. 开始render阶段
  let exitStatus = renderRootSync(root, lanes);
  
  // 3. 如果render阶段完成,进入commit阶段
  if (exitStatus === RootCompleted) {
    const finishedWork = root.current.alternate;
    root.finishedWork = finishedWork;
    root.finishedLanes = lanes;
    commitRoot(root, workInProgressRootRecoverableErrors);
  }
  
  // 4. 恢复执行上下文
  executionContext = prevExecutionContext;
  
  return null;
}

同步渲染的特点

  1. 不可中断:一旦开始,必须完成整个render阶段
  2. 优先级最高:会阻塞其他更新
  3. 适用场景:用户输入、flushSync调用

6.1.2 performConcurrentWorkOnRoot

当有并发更新时,React会调用performConcurrentWorkOnRoot函数:

javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
// 行号:900-1000(React 19.3.0)

function performConcurrentWorkOnRoot(root: FiberRoot, didTimeout: boolean) {
  // 并发渲染的入口
  
  // 1. 获取要渲染的lanes
  const lanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  
  if (lanes === NoLanes) {
    return null;
  }
  
  // 2. 判断是否应该使用时间切片
  const shouldTimeSlice =
    !includesBlockingLane(root, lanes) &&
    !includesExpiredLane(root, lanes) &&
    !didTimeout;
  
  // 3. 开始render阶段
  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);
  
  // 4. 处理render阶段的结果
  if (exitStatus !== RootInProgress) {
    if (exitStatus === RootCompleted) {
      // render阶段完成,准备commit
      const finishedWork = root.current.alternate;
      root.finishedWork = finishedWork;
      root.finishedLanes = lanes;
      finishConcurrentRender(root, exitStatus, finishedWork, lanes);
    }
  }
  
  // 5. 检查是否还有其他工作
  ensureRootIsScheduled(root);
  
  return getContinuationForRoot(root, originalCallbackNode);
}

并发渲染的特点

  1. 可中断:可以在时间片用完时暂停
  2. 优先级调度:高优先级更新可以打断低优先级更新
  3. 适用场景:数据获取、过渡更新、空闲更新

同步渲染 vs 并发渲染

同步渲染:
[████████████████████████████████████] render阶段(不可中断)

                                    commit阶段

并发渲染:
[█] render 5ms
   [█] render 5ms
      [█] render 5ms
         ↑ 可以被高优先级更新打断
            [█] render 5ms
               ...

                commit阶段

6.1.3 workLoop工作循环

无论是同步还是并发渲染,最终都会进入workLoop工作循环:

javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
// 行号:1800-1850(React 19.3.0)

function workLoopSync() {
  // 同步工作循环:一直执行到完成
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopConcurrent() {
  // 并发工作循环:检查是否应该让出控制权
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber): void {
  // 获取current Fiber(旧的Fiber节点)
  const current = unitOfWork.alternate;
  
  // 1. beginWork阶段:处理当前Fiber节点
  let next = beginWork(current, unitOfWork, renderLanes);
  
  // 2. 更新memoizedProps
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  
  if (next === null) {
    // 3. 如果没有子节点,进入completeWork阶段
    completeUnitOfWork(unitOfWork);
  } else {
    // 4. 如果有子节点,继续处理子节点
    workInProgress = next;
  }
}

工作循环的流程

Fiber树的遍历顺序

React使用深度优先遍历(DFS)来遍历Fiber树:

Fiber树结构:
        App
       /   \
    Header  Main
     /       /  \
  Logo    List  Sidebar
          /  \
       Item1 Item2

遍历顺序(深度优先):
1. App (beginWork)
2. Header (beginWork)
3. Logo (beginWork)
4. Logo (completeWork)
5. Header (completeWork)
6. Main (beginWork)
7. List (beginWork)
8. Item1 (beginWork)
9. Item1 (completeWork)
10. Item2 (beginWork)
11. Item2 (completeWork)
12. List (completeWork)
13. Sidebar (beginWork)
14. Sidebar (completeWork)
15. Main (completeWork)
16. App (completeWork)

render阶段的两个子阶段

render阶段分为两个子阶段:

  1. beginWork阶段(向下遍历)

    • 创建或复用子Fiber节点
    • 执行Diff算法
    • 标记副作用flags
  2. completeWork阶段(向上归并)

    • 创建或更新DOM节点
    • 处理props
    • 收集副作用链
向下遍历(beginWork):
App → Header → Logo

向上归并(completeWork):
Logo → Header → App

6.2 beginWork阶段

beginWork是render阶段的核心函数,它负责处理每个Fiber节点,决定如何更新子节点。

6.2.1 beginWork函数入口

javascript
// 文件:packages/react-reconciler/src/ReactFiberBeginWork.js
// 行号:4150-4250(React 19.3.0)

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // current: 旧的Fiber节点(上一次渲染的结果)
  // workInProgress: 新的Fiber节点(正在构建的Fiber树)
  // renderLanes: 当前渲染的优先级
  
  if (current !== null) {
    // 更新流程
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged()
    ) {
      // props或context发生变化,需要更新
      didReceiveUpdate = true;
    } else {
      // props和context都没变化,检查是否可以复用
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
        current,
        renderLanes,
      );
      
      if (!hasScheduledUpdateOrContext) {
        // 没有更新,可以bailout(跳过)
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
      
      didReceiveUpdate = false;
    }
  } else {
    // 首次渲染
    didReceiveUpdate = false;
  }
  
  // 清空lanes,表示这个Fiber节点正在被处理
  workInProgress.lanes = NoLanes;
  
  // 根据tag类型分发处理
  switch (workInProgress.tag) {
    case FunctionComponent:
      return updateFunctionComponent(
        current,
        workInProgress,
        workInProgress.type,
        workInProgress.pendingProps,
        renderLanes,
      );
    case ClassComponent:
      return updateClassComponent(
        current,
        workInProgress,
        workInProgress.type,
        workInProgress.pendingProps,
        renderLanes,
      );
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);
    // ... 其他类型
  }
}

beginWork的三个关键决策

  1. 是否可以复用(bailout)

    • 如果props没变、context没变、没有更新,可以跳过
    • 这是React性能优化的关键
  2. 如何处理当前节点

    • 根据tag类型调用不同的update函数
    • 执行组件逻辑(函数组件、类组件)
  3. 返回什么

    • 返回子Fiber节点(继续向下遍历)
    • 返回null(没有子节点,进入completeWork)

6.2.2 根据tag分发处理

beginWork根据Fiber节点的tag类型,调用不同的update函数。

updateFunctionComponent(函数组件)

javascript
// 文件:packages/react-reconciler/src/ReactFiberBeginWork.js
// 行号:1100-1150(React 19.3.0)

function updateFunctionComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  // 1. 准备context
  prepareToReadContext(workInProgress, renderLanes);
  
  // 2. 执行函数组件,获取children
  let nextChildren;
  if (__DEV__) {
    // 开发环境:设置当前正在渲染的Fiber
    ReactCurrentOwner.current = workInProgress;
    nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
    );
  } else {
    nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
    );
  }
  
  if (current !== null && !didReceiveUpdate) {
    // 3. 如果可以bailout,复用子节点
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  
  // 4. 协调子节点
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  
  // 5. 返回子Fiber节点
  return workInProgress.child;
}

updateHostComponent(原生DOM组件)

javascript
// 文件:packages/react-reconciler/src/ReactFiberBeginWork.js
// 行号:1929-1980(React 19.3.0)

function updateHostComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  // 1. 获取type和props
  const type = workInProgress.type;
  const nextProps = workInProgress.pendingProps;
  const prevProps = current !== null ? current.memoizedProps : null;
  
  // 2. 获取children
  let nextChildren = nextProps.children;
  
  // 3. 判断是否是文本节点
  const isDirectTextChild = shouldSetTextContent(type, nextProps);
  
  if (isDirectTextChild) {
    // 如果children是纯文本,不需要创建子Fiber
    nextChildren = null;
  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
    // 之前是文本,现在不是,需要标记ContentReset
    workInProgress.flags |= ContentReset;
  }
  
  // 4. 标记ref
  markRef(current, workInProgress);
  
  // 5. 协调子节点
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  
  // 6. 返回子Fiber节点
  return workInProgress.child;
}

reconcileChildren(协调子节点)

javascript
// 文件:packages/react-reconciler/src/ReactFiberBeginWork.js
// 行号:280-310(React 19.3.0)

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    // 首次渲染:直接创建子Fiber
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 更新:执行Diff算法
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

mountChildFibers vs reconcileChildFibers

这两个函数实际上是同一个函数的不同配置:

javascript
// 文件:packages/react-reconciler/src/ReactChildFiber.js
// 行号:1800-1850(React 19.3.0)

function createChildReconciler(shouldTrackSideEffects: boolean) {
  // shouldTrackSideEffects: 是否追踪副作用
  
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    // 根据newChild的类型,调用不同的处理函数
    
    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          // 单个ReactElement
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_PORTAL_TYPE:
          // Portal
          return placeSingleChild(
            reconcileSinglePortal(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
      }
      
      if (isArray(newChild)) {
        // 数组children
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }
    }
    
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      // 文本节点
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild,
          lanes,
        ),
      );
    }
    
    // 其他情况:删除所有旧子节点
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }
  
  return reconcileChildFibers;
}

// 首次渲染:不追踪副作用(因为整个树都是新的)
export const mountChildFibers = createChildReconciler(false);

// 更新:追踪副作用(需要标记Placement、Update、Deletion)
export const reconcileChildFibers = createChildReconciler(true);

为什么首次渲染不追踪副作用?

首次渲染时,整个Fiber树都是新的,不需要标记Placement flag。只有根节点需要标记Placement,在commit阶段一次性插入整个DOM树。这样可以减少DOM操作次数,提高性能。

6.2.3 bailout优化路径

bailout是React的重要性能优化,当一个组件没有变化时,可以跳过它及其子树的渲染。

bailoutOnAlreadyFinishedWork

javascript
// 文件:packages/react-reconciler/src/ReactFiberBeginWork.js
// 行号:4050-4100(React 19.3.0)

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    // 复用旧的依赖
    workInProgress.dependencies = current.dependencies;
  }
  
  // 标记跳过的lanes
  markSkippedUpdateLanes(workInProgress.lanes);
  
  // 检查子节点是否有工作
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    // 子节点也没有工作,整个子树都可以跳过
    return null;
  }
  
  // 子节点有工作,需要继续处理子节点
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}

bailout的条件

一个组件可以bailout,需要满足以下所有条件:

  1. oldProps === newProps:props没有变化
  2. context没有变化:使用的context值没有变化
  3. 没有内部更新:组件内部没有调用setState
  4. type没有变化:组件类型没有变化(热更新场景)
jsx
// 示例:React.memo实现bailout

const ExpensiveComponent = React.memo(function ExpensiveComponent({ value }) {
  console.log('ExpensiveComponent render');
  // 耗时的渲染逻辑
  return <div>{value}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [value] = useState('constant');
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      {/* value没变,ExpensiveComponent会bailout */}
      <ExpensiveComponent value={value} />
    </div>
  );
}

childLanes的作用

childLanes记录了子树中所有待处理的lanes。如果childLanes为空,说明整个子树都没有工作,可以完全跳过。

Fiber树:
        App (lanes: 0, childLanes: DefaultLane)
       /   \
    Header  Main (lanes: DefaultLane, childLanes: 0)
     /       /  \
  Logo    List  Sidebar
          /  \
       Item1 Item2

解释:
- App的childLanes包含DefaultLane,因为Main有更新
- Header的childLanes为0,可以完全跳过Header及其子树
- Main有更新,需要处理

cloneChildFibers

当父节点可以bailout,但子节点有工作时,需要克隆子Fiber节点:

javascript
// 文件:packages/react-reconciler/src/ReactChildFiber.js

export function cloneChildFibers(
  current: Fiber | null,
  workInProgress: Fiber,
): void {
  if (workInProgress.child === null) {
    return;
  }
  
  let currentChild = workInProgress.child;
  let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
  workInProgress.child = newChild;
  
  newChild.return = workInProgress;
  while (currentChild.sibling !== null) {
    currentChild = currentChild.sibling;
    newChild = newChild.sibling = createWorkInProgress(
      currentChild,
      currentChild.pendingProps,
    );
    newChild.return = workInProgress;
  }
  newChild.sibling = null;
}

6.3 Diff算法详解

Diff算法是React协调过程的核心,它负责比较新旧子节点,找出最小的变更操作。

6.3.1 单节点Diff

当新的children只有一个节点时,执行单节点Diff。

reconcileSingleElement

javascript
// 文件:packages/react-reconciler/src/ReactChildFiber.js
// 行号:1300-1400(React 19.3.0)

function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement,
  lanes: Lanes,
): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  
  // 1. 遍历旧的子节点,查找可以复用的节点
  while (child !== null) {
    if (child.key === key) {
      // key相同,检查type
      const elementType = element.type;
      if (child.elementType === elementType) {
        // key和type都相同,可以复用
        deleteRemainingChildren(returnFiber, child.sibling);
        
        const existing = useFiber(child, element.props);
        existing.ref = coerceRef(returnFiber, child, element);
        existing.return = returnFiber;
        return existing;
      }
      // key相同但type不同,删除所有旧节点
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      // key不同,删除这个旧节点
      deleteChild(returnFiber, child);
    }
    child = child.sibling;
  }
  
  // 2. 没有可复用的节点,创建新节点
  const created = createFiberFromElement(element, returnFiber.mode, lanes);
  created.ref = coerceRef(returnFiber, currentFirstChild, element);
  created.return = returnFiber;
  return created;
}

单节点Diff的流程

示例:单节点Diff

jsx
// 场景1:key和type都相同,复用
// 旧:<div key="a">old</div>
// 新:<div key="a">new</div>
// 结果:复用div节点,更新props

// 场景2:key相同,type不同,不能复用
// 旧:<div key="a">old</div>
// 新:<span key="a">new</span>
// 结果:删除div,创建span

// 场景3:key不同,不能复用
// 旧:<div key="a">old</div>
// 新:<div key="b">new</div>
// 结果:删除key="a"的div,创建key="b"的div

6.3.2 多节点Diff

当新的children是数组时,执行多节点Diff。这是React Diff算法最复杂的部分。

reconcileChildrenArray

javascript
// 文件:packages/react-reconciler/src/ReactChildFiber.js
// 行号:800-1200(React 19.3.0)

function reconcileChildrenArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildren: Array<any>,
  lanes: Lanes,
): Fiber | null {
  // 返回的第一个子Fiber
  let resultingFirstChild: Fiber | null = null;
  // 上一个新Fiber
  let previousNewFiber: Fiber | null = null;
  
  // 旧的子Fiber
  let oldFiber = currentFirstChild;
  // 上一个可复用的位置
  let lastPlacedIndex = 0;
  // 新children的索引
  let newIdx = 0;
  // 下一个旧Fiber
  let nextOldFiber = null;
  
  // ===== 第一轮遍历:处理更新的节点 =====
  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) {
      // key不同,无法复用,跳出第一轮遍历
      if (oldFiber === null) {
        oldFiber = nextOldFiber;
      }
      break;
    }
    
    if (shouldTrackSideEffects) {
      if (oldFiber && newFiber.alternate === null) {
        // 没有复用旧节点,删除它
        deleteChild(returnFiber, oldFiber);
      }
    }
    
    // 标记位置
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    
    // 构建新Fiber链表
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }
  
  // ===== 第二轮遍历:处理剩余情况 =====
  
  if (newIdx === newChildren.length) {
    // 新children遍历完了,删除剩余的旧节点
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }
  
  if (oldFiber === null) {
    // 旧节点遍历完了,创建剩余的新节点
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      if (newFiber === null) {
        continue;
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    return resultingFirstChild;
  }
  
  // ===== 第三轮遍历:处理移动的节点 =====
  
  // 将剩余的旧节点放入Map
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
  
  // 遍历剩余的新children
  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx],
      lanes,
    );
    
    if (newFiber !== null) {
      if (shouldTrackSideEffects) {
        if (newFiber.alternate !== null) {
          // 复用了旧节点,从Map中删除
          existingChildren.delete(
            newFiber.key === null ? newIdx : newFiber.key,
          );
        }
      }
      
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
  }
  
  if (shouldTrackSideEffects) {
    // 删除Map中剩余的旧节点
    existingChildren.forEach(child => deleteChild(returnFiber, child));
  }
  
  return resultingFirstChild;
}

多节点Diff的三轮遍历

React的多节点Diff算法分为三轮遍历,每轮处理不同的情况:

第一轮遍历:处理更新的节点

从头开始遍历,逐个比较新旧节点:

  • 如果key相同,尝试复用
  • 如果key不同,跳出第一轮遍历
旧:a b c d
新:a b e f

第一轮遍历:
- a vs a:key相同,复用 ✓
- b vs b:key相同,复用 ✓
- c vs e:key不同,跳出 ✗

第二轮遍历:处理剩余情况

根据第一轮遍历的结果,处理剩余节点:

情况1:新children遍历完了

旧:a b c d
新:a b

结果:删除c、d

情况2:旧节点遍历完了

旧:a b
新:a b c d

结果:创建c、d

情况3:都没遍历完

旧:a b c d
新:a b e f

结果:进入第三轮遍历

第三轮遍历:处理移动的节点

将剩余的旧节点放入Map(key -> Fiber),然后遍历剩余的新children,从Map中查找可复用的节点。

旧:a b c d
新:a b d c

第一轮遍历:a、b复用
剩余旧节点:c、d
剩余新节点:d、c

第三轮遍历:
1. 创建Map:{ c: Fiber(c), d: Fiber(d) }
2. 处理d:从Map中找到Fiber(d),复用
3. 处理c:从Map中找到Fiber(c),复用

placeChild(标记位置)

javascript
// 文件:packages/react-reconciler/src/ReactChildFiber.js

function placeChild(
  newFiber: Fiber,
  lastPlacedIndex: number,
  newIndex: number,
): number {
  newFiber.index = newIndex;
  
  if (!shouldTrackSideEffects) {
    // 首次渲染,不需要标记
    return lastPlacedIndex;
  }
  
  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;
  }
}

lastPlacedIndex的作用

lastPlacedIndex记录了最后一个可复用节点在旧列表中的位置。如果一个节点在旧列表中的位置小于lastPlacedIndex,说明它需要向右移动。

示例:
旧:a(0) b(1) c(2) d(3)
新:a    c    b    d

处理过程:
1. a:oldIndex=0, lastPlacedIndex=0, 不移动, lastPlacedIndex=0
2. c:oldIndex=2, lastPlacedIndex=0, 不移动, lastPlacedIndex=2
3. b:oldIndex=1, lastPlacedIndex=2, 需要移动(1 < 2)
4. d:oldIndex=3, lastPlacedIndex=2, 不移动, lastPlacedIndex=3

结果:只需要移动b

6.3.3 key的作用与最佳实践

key是React Diff算法的核心,它帮助React识别哪些元素发生了变化。

为什么需要key?

没有key时,React只能按位置比较:

jsx
// 没有key
旧:<li>A</li> <li>B</li> <li>C</li>
新:<li>B</li> <li>C</li> <li>D</li>

React的处理:
- 位置0:A -> B(更新)
- 位置1:B -> C(更新)
- 位置2:C -> D(更新)
结果:3次更新

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

React的处理:
- key="a":删除
- key="b":复用
- key="c":复用
- key="d":创建
结果:1次删除 + 1次创建,性能更好

key的最佳实践

  1. 使用稳定的唯一标识
jsx
// ✓ 好:使用数据的唯一ID
{items.map(item => (
  <Item key={item.id} data={item} />
))}

// ✗ 差:使用索引(数据顺序变化时会有问题)
{items.map((item, index) => (
  <Item key={index} data={item} />
))}

// ✗ 差:使用随机值(每次渲染都会重新创建)
{items.map(item => (
  <Item key={Math.random()} data={item} />
))}
  1. key应该在兄弟节点中唯一
jsx
// ✓ 好:key在同一层级中唯一
<ul>
  <li key="1">Item 1</li>
  <li key="2">Item 2</li>
</ul>
<ul>
  <li key="1">Item 1</li> {/* 可以重复,因为不在同一层级 */}
  <li key="2">Item 2</li>
</ul>

// ✗ 差:key在同一层级中重复
<ul>
  <li key="1">Item 1</li>
  <li key="1">Item 2</li> {/* 重复的key */}
</ul>
  1. 不要使用可能变化的值作为key
jsx
// ✗ 差:使用可能变化的值
{items.map(item => (
  <Item key={item.name} data={item} /> // name可能重复或变化
))}

// ✓ 好:使用稳定的ID
{items.map(item => (
  <Item key={item.id} data={item} />
))}

使用索引作为key的问题

jsx
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', done: false },
    { id: 2, text: 'Learn Diff', done: false },
    { id: 3, text: 'Build App', done: false },
  ]);
  
  // ✗ 使用索引作为key
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index}>
          <input type="checkbox" checked={todo.done} />
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

// 问题:当删除第一项时
// 旧:[0: Learn React, 1: Learn Diff, 2: Build App]
// 新:[0: Learn Diff, 1: Build App]
// React会认为:
// - 索引0:Learn React -> Learn Diff(更新)
// - 索引1:Learn Diff -> Build App(更新)
// - 索引2:删除
// 结果:checkbox的状态会错乱

正确的做法

jsx
// ✓ 使用稳定的ID作为key
return (
  <ul>
    {todos.map(todo => (
      <li key={todo.id}>
        <input type="checkbox" checked={todo.done} />
        {todo.text}
      </li>
    ))}
  </ul>
);

// 删除第一项时:
// 旧:[1: Learn React, 2: Learn Diff, 3: Build App]
// 新:[2: Learn Diff, 3: Build App]
// React会认为:
// - id=1:删除
// - id=2:复用
// - id=3:复用
// 结果:checkbox的状态正确

6.3.4 示例:Diff算法图解

让我们通过一个完整的示例来理解Diff算法的工作过程。

场景:列表重排序

jsx
// 旧列表
<ul>
  <li key="a">A</li>
  <li key="b">B</li>
  <li key="c">C</li>
  <li key="d">D</li>
</ul>

// 新列表
<ul>
  <li key="d">D</li>
  <li key="a">A</li>
  <li key="c">C</li>
  <li key="e">E</li>
</ul>

Diff过程

详细步骤

初始状态:
旧:a(0) b(1) c(2) d(3)
新:d    a    c    e

第一轮遍历:
- 比较 a vs d:key不同,跳出

第二轮遍历:
- 新旧都没遍历完,进入第三轮

第三轮遍历:
1. 创建Map:
   { a: Fiber(a), b: Fiber(b), c: Fiber(c), d: Fiber(d) }

2. 处理新节点d:
   - 从Map中找到Fiber(d),复用
   - oldIndex=3, lastPlacedIndex=0
   - 3 >= 0,不需要移动
   - lastPlacedIndex = 3
   - 从Map中删除d

3. 处理新节点a:
   - 从Map中找到Fiber(a),复用
   - oldIndex=0, lastPlacedIndex=3
   - 0 < 3,需要移动(标记Placement)
   - lastPlacedIndex = 3
   - 从Map中删除a

4. 处理新节点c:
   - 从Map中找到Fiber(c),复用
   - oldIndex=2, lastPlacedIndex=3
   - 2 < 3,需要移动(标记Placement)
   - lastPlacedIndex = 3
   - 从Map中删除c

5. 处理新节点e:
   - Map中没有,创建新Fiber
   - 标记Placement
   - lastPlacedIndex = 3

6. 删除Map中剩余的节点:
   - 删除b(标记Deletion)

最终结果:
- d:复用,不移动
- a:复用,移动
- c:复用,移动
- e:创建
- b:删除

可视化

旧DOM:
┌───┬───┬───┬───┐
│ a │ b │ c │ d │
└───┴───┴───┴───┘
  0   1   2   3

新DOM:
┌───┬───┬───┬───┐
│ d │ a │ c │ e │
└───┴───┴───┴───┘

操作:
1. d不动(已经在最右边)
2. a向右移动到d后面
3. c向右移动到a后面
4. e插入到c后面
5. b删除

为什么这样设计?

React的Diff算法有一个重要的优化原则:尽量减少移动次数

在上面的例子中,React选择移动a和c,而不是移动d。这是因为:

  • 移动a和c:2次移动
  • 移动d:1次移动,但需要移动到最前面,可能涉及更多DOM操作

React通过lastPlacedIndex来实现这个优化:只移动那些在旧列表中位置小于lastPlacedIndex的节点。


6.4 completeWork阶段

completeWork是render阶段的第二个子阶段,它在beginWork向下遍历完成后,向上归并时执行。

6.4.1 创建DOM节点

completeWork的主要职责之一是创建DOM节点。

javascript
// 文件:packages/react-reconciler/src/ReactFiberCompleteWork.js
// 行号:800-1200(React 19.3.0)

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  
  switch (workInProgress.tag) {
    case HostComponent: {
      // 原生DOM组件
      const type = workInProgress.type;
      
      if (current !== null && workInProgress.stateNode != null) {
        // 更新流程
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          renderLanes,
        );
        
        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
      } else {
        // 首次渲染流程
        
        // 1. 创建DOM实例
        const instance = createInstance(
          type,
          newProps,
          rootContainerInstance,
          currentHostContext,
          workInProgress,
        );
        
        // 2. 将子节点append到DOM实例
        appendAllChildren(instance, workInProgress, false, false);
        
        // 3. 保存DOM实例到stateNode
        workInProgress.stateNode = instance;
        
        // 4. 初始化DOM属性
        if (
          finalizeInitialChildren(
            instance,
            type,
            newProps,
            currentHostContext,
          )
        ) {
          markUpdate(workInProgress);
        }
        
        if (workInProgress.ref !== null) {
          markRef(workInProgress);
        }
      }
      
      bubbleProperties(workInProgress);
      return null;
    }
    
    case HostText: {
      // 文本节点
      const newText = newProps;
      
      if (current && workInProgress.stateNode != null) {
        // 更新文本
        const oldText = current.memoizedProps;
        updateHostText(current, workInProgress, oldText, newText);
      } else {
        // 创建文本节点
        workInProgress.stateNode = createTextInstance(
          newText,
          rootContainerInstance,
          currentHostContext,
          workInProgress,
        );
      }
      
      bubbleProperties(workInProgress);
      return null;
    }
    
    case FunctionComponent:
    case ClassComponent:
    case HostRoot:
      // 这些类型不需要创建DOM,只需要冒泡属性
      bubbleProperties(workInProgress);
      return null;
    
    // ... 其他类型
  }
}

createInstance(创建DOM元素)

javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMHostConfig.js

export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
  const domElement = document.createElement(type);
  
  // 保存Fiber引用到DOM元素
  precacheFiberNode(internalInstanceHandle, domElement);
  updateFiberProps(domElement, props);
  
  return domElement;
}

appendAllChildren(添加子节点)

javascript
// 文件:packages/react-reconciler/src/ReactFiberCompleteWork.js

appendAllChildren = function(
  parent: Instance,
  workInProgress: Fiber,
  needsVisibilityToggle: boolean,
  isHidden: boolean,
) {
  let node = workInProgress.child;
  while (node !== null) {
    if (node.tag === HostComponent || node.tag === HostText) {
      // 子节点是DOM节点,直接append
      appendInitialChild(parent, node.stateNode);
    } else if (node.child !== null) {
      // 子节点是组件,继续向下查找DOM节点
      node.child.return = node;
      node = node.child;
      continue;
    }
    
    if (node === workInProgress) {
      return;
    }
    
    while (node.sibling === null) {
      if (node.return === null || node.return === workInProgress) {
        return;
      }
      node = node.return;
    }
    
    node.sibling.return = node.return;
    node = node.sibling;
  }
};

为什么在completeWork中创建DOM?

在completeWork阶段创建DOM有几个优势:

  1. 子节点已经创建:向上归并时,子节点的DOM已经创建,可以直接append
  2. 减少DOM操作:可以一次性构建完整的DOM子树,然后在commit阶段一次性插入
  3. 支持Suspense:如果子树中有Suspense,可以延迟创建DOM

6.4.2 属性处理

completeWork还负责处理DOM属性的初始化和更新。

finalizeInitialChildren(初始化属性)

javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMHostConfig.js

export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  hostContext: HostContext,
): boolean {
  // 设置DOM属性
  setInitialProperties(domElement, type, props);
  
  // 返回是否需要自动聚焦
  switch (type) {
    case 'button':
    case 'input':
    case 'select':
    case 'textarea':
      return !!props.autoFocus;
    case 'img':
      return true;
    default:
      return false;
  }
}

setInitialProperties(设置初始属性)

javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMComponent.js

export function setInitialProperties(
  domElement: Element,
  tag: string,
  props: Object,
): void {
  // 1. 处理特殊标签
  switch (tag) {
    case 'input':
      ReactDOMInputInitWrapperState(domElement, props);
      break;
    case 'textarea':
      ReactDOMTextareaInitWrapperState(domElement, props);
      break;
    case 'select':
      ReactDOMSelectInitWrapperState(domElement, props);
      break;
    // ... 其他特殊标签
  }
  
  // 2. 设置通用属性
  setInitialDOMProperties(tag, domElement, props);
  
  // 3. 处理特殊标签的后续逻辑
  switch (tag) {
    case 'input':
      ReactDOMInputPostMountWrapper(domElement, props);
      break;
    case 'textarea':
      ReactDOMTextareaPostMountWrapper(domElement, props);
      break;
    case 'select':
      ReactDOMSelectPostMountWrapper(domElement, props);
      break;
  }
}

setInitialDOMProperties(设置DOM属性)

javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMComponent.js

function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  nextProps: Object,
): void {
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    const nextProp = nextProps[propKey];
    
    if (propKey === 'style') {
      // 处理style
      setValueForStyles(domElement, nextProp);
    } else if (propKey === 'dangerouslySetInnerHTML') {
      // 处理innerHTML
      const nextHtml = nextProp ? nextProp.__html : undefined;
      if (nextHtml != null) {
        setInnerHTML(domElement, nextHtml);
      }
    } else if (propKey === 'children') {
      // 处理children
      if (typeof nextProp === 'string') {
        setTextContent(domElement, nextProp);
      } else if (typeof nextProp === 'number') {
        setTextContent(domElement, '' + nextProp);
      }
    } else if (propKey === 'suppressContentEditableWarning' ||
               propKey === 'suppressHydrationWarning') {
      // 忽略这些属性
    } else if (propKey === 'autoFocus') {
      // autoFocus在commit阶段处理
    } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      // 事件属性在commit阶段处理
    } else {
      // 其他属性
      setValueForProperty(domElement, propKey, nextProp);
    }
  }
}

updateHostComponent(更新属性)

javascript
// 文件:packages/react-reconciler/src/ReactFiberCompleteWork.js

updateHostComponent = function(
  current: Fiber,
  workInProgress: Fiber,
  type: Type,
  newProps: Props,
  renderLanes: Lanes,
) {
  const oldProps = current.memoizedProps;
  if (oldProps === newProps) {
    // props没变,不需要更新
    return;
  }
  
  const instance: Instance = workInProgress.stateNode;
  
  // 计算需要更新的属性
  const updatePayload = prepareUpdate(
    instance,
    type,
    oldProps,
    newProps,
    renderLanes,
  );
  
  // 保存updatePayload到updateQueue
  workInProgress.updateQueue = (updatePayload: any);
  
  if (updatePayload) {
    // 标记Update flag
    markUpdate(workInProgress);
  }
};

prepareUpdate(计算属性差异)

javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMHostConfig.js

export function prepareUpdate(
  domElement: Instance,
  type: string,
  oldProps: Props,
  newProps: Props,
): null | Array<mixed> {
  return diffProperties(
    domElement,
    type,
    oldProps,
    newProps,
  );
}

diffProperties(对比属性)

javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMComponent.js

export function diffProperties(
  domElement: Element,
  tag: string,
  lastProps: Object,
  nextProps: Object,
): null | Array<mixed> {
  let updatePayload: null | Array<any> = null;
  
  let propKey;
  let styleName;
  let styleUpdates = null;
  
  // 1. 遍历旧props,找出被删除的属性
  for (propKey in lastProps) {
    if (
      nextProps.hasOwnProperty(propKey) ||
      !lastProps.hasOwnProperty(propKey) ||
      lastProps[propKey] == null
    ) {
      continue;
    }
    
    if (propKey === 'style') {
      // style被删除
      const lastStyle = lastProps[propKey];
      for (styleName in lastStyle) {
        if (lastStyle.hasOwnProperty(styleName)) {
          if (!styleUpdates) {
            styleUpdates = {};
          }
          styleUpdates[styleName] = '';
        }
      }
    } else {
      // 其他属性被删除
      (updatePayload = updatePayload || []).push(propKey, null);
    }
  }
  
  // 2. 遍历新props,找出新增或变化的属性
  for (propKey in nextProps) {
    const nextProp = nextProps[propKey];
    const lastProp = lastProps != null ? lastProps[propKey] : undefined;
    
    if (
      !nextProps.hasOwnProperty(propKey) ||
      nextProp === lastProp ||
      (nextProp == null && lastProp == null)
    ) {
      continue;
    }
    
    if (propKey === 'style') {
      // style变化
      if (lastProp) {
        // 找出被删除的style
        for (styleName in lastProp) {
          if (
            lastProp.hasOwnProperty(styleName) &&
            (!nextProp || !nextProp.hasOwnProperty(styleName))
          ) {
            if (!styleUpdates) {
              styleUpdates = {};
            }
            styleUpdates[styleName] = '';
          }
        }
        // 找出新增或变化的style
        for (styleName in nextProp) {
          if (
            nextProp.hasOwnProperty(styleName) &&
            lastProp[styleName] !== nextProp[styleName]
          ) {
            if (!styleUpdates) {
              styleUpdates = {};
            }
            styleUpdates[styleName] = nextProp[styleName];
          }
        }
      } else {
        // 新增style
        if (!styleUpdates) {
          if (!updatePayload) {
            updatePayload = [];
          }
          updatePayload.push(propKey, styleUpdates);
        }
        styleUpdates = nextProp;
      }
    } else if (propKey === 'dangerouslySetInnerHTML') {
      const nextHtml = nextProp ? nextProp.__html : undefined;
      const lastHtml = lastProp ? lastProp.__html : undefined;
      if (nextHtml != null && lastHtml !== nextHtml) {
        (updatePayload = updatePayload || []).push(propKey, nextHtml);
      }
    } else if (propKey === 'children') {
      if (typeof nextProp === 'string' || typeof nextProp === 'number') {
        (updatePayload = updatePayload || []).push(propKey, '' + nextProp);
      }
    } else {
      // 其他属性
      (updatePayload = updatePayload || []).push(propKey, nextProp);
    }
  }
  
  if (styleUpdates) {
    (updatePayload = updatePayload || []).push('style', styleUpdates);
  }
  
  return updatePayload;
}

updatePayload的格式

updatePayload是一个数组,格式为:[key1, value1, key2, value2, ...]

javascript
// 示例
const updatePayload = [
  'className', 'button active',
  'style', { color: 'red', fontSize: '14px' },
  'disabled', true,
];

// 在commit阶段,会遍历这个数组,更新DOM属性
for (let i = 0; i < updatePayload.length; i += 2) {
  const propKey = updatePayload[i];
  const propValue = updatePayload[i + 1];
  setValueForProperty(domElement, propKey, propValue);
}

6.4.3 effectList的构建

在React 17之前,completeWork阶段会构建effectList(副作用链表),用于在commit阶段快速遍历有副作用的节点。

React 18之后,effectList被移除,改为在commit阶段遍历整个Fiber树。但理解effectList的概念仍然有助于理解React的工作原理。

bubbleProperties(冒泡属性)

javascript
// 文件:packages/react-reconciler/src/ReactFiberCompleteWork.js

function bubbleProperties(completedWork: Fiber) {
  const didBailout =
    completedWork.alternate !== null &&
    completedWork.alternate.child === completedWork.child;
  
  let newChildLanes = NoLanes;
  let subtreeFlags = NoFlags;
  
  if (!didBailout) {
    // 没有bailout,需要收集子节点的flags和lanes
    let child = completedWork.child;
    while (child !== null) {
      // 合并子节点的lanes
      newChildLanes = mergeLanes(
        newChildLanes,
        mergeLanes(child.lanes, child.childLanes),
      );
      
      // 合并子节点的flags
      subtreeFlags |= child.subtreeFlags;
      subtreeFlags |= child.flags;
      
      child.return = completedWork;
      child = child.sibling;
    }
    
    completedWork.subtreeFlags |= subtreeFlags;
  } else {
    // bailout了,直接复用
    let child = completedWork.child;
    while (child !== null) {
      newChildLanes = mergeLanes(
        newChildLanes,
        mergeLanes(child.lanes, child.childLanes),
      );
      
      subtreeFlags |= child.subtreeFlags & StaticMask;
      subtreeFlags |= child.flags & StaticMask;
      
      child.return = completedWork;
      child = child.sibling;
    }
    
    completedWork.subtreeFlags |= subtreeFlags;
  }
  
  completedWork.childLanes = newChildLanes;
  
  return didBailout;
}

subtreeFlags的作用

subtreeFlags记录了子树中所有节点的flags。在commit阶段,可以通过检查subtreeFlags来判断是否需要遍历子树。

javascript
// 在commit阶段
function commitMutationEffects(root, finishedWork) {
  if ((finishedWork.subtreeFlags & MutationMask) !== NoFlags) {
    // 子树有mutation副作用,需要遍历
    commitMutationEffectsOnFiber(finishedWork.child, root);
  }
  
  if ((finishedWork.flags & MutationMask) !== NoFlags) {
    // 当前节点有mutation副作用,需要处理
    commitMutationEffectsOnFiber(finishedWork, root);
  }
}

flags的类型

javascript
// 文件:packages/react-reconciler/src/ReactFiberFlags.js

export const NoFlags = 0b0000000000000000000000000000;
export const PerformedWork = 0b0000000000000000000000000001;
export const Placement = 0b0000000000000000000000000010;
export const Update = 0b0000000000000000000000000100;
export const Deletion = 0b0000000000000000000000001000;
export const ChildDeletion = 0b0000000000000000000000010000;
export const ContentReset = 0b0000000000000000000000100000;
export const Callback = 0b0000000000000000000001000000;
export const DidCapture = 0b0000000000000000000010000000;
export const Ref = 0b0000000000000000000100000000;
export const Snapshot = 0b0000000000000000001000000000;
export const Passive = 0b0000000000000000010000000000;
// ... 更多flags

// flags的组合
export const MutationMask =
  Placement |
  Update |
  ChildDeletion |
  ContentReset |
  Ref |
  Hydrating |
  Visibility;

export const LayoutMask = Update | Callback | Ref | Visibility;

export const PassiveMask = Passive | ChildDeletion;

completeWork的完整流程


6.5 示例:Diff算法图解

让我们通过一个完整的示例,从头到尾演示React的协调过程。

示例代码

jsx
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', done: false },
    { id: 2, text: 'Learn Diff', done: false },
    { id: 3, text: 'Build App', done: false },
  ]);
  
  const handleToggle = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  };
  
  const handleDelete = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };
  
  const handleReorder = () => {
    setTodos([todos[2], todos[0], todos[1]]);
  };
  
  return (
    <div>
      <button onClick={handleReorder}>重排序</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => handleToggle(todo.id)}
            />
            <span>{todo.text}</span>
            <button onClick={() => handleDelete(todo.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

场景1:切换checkbox

用户点击第2项的checkbox

触发更新:
- 调用setTodos
- 创建更新对象,加入updateQueue
- 调度更新(DefaultLane)

render阶段:
1. beginWork(TodoList)
   - 执行函数组件
   - 计算新的todos state
   - 返回children(div)

2. beginWork(div)
   - 协调children(button, ul)
   - 返回button

3. beginWork(button)
   - props没变,bailout
   - 返回null

4. completeWork(button)
   - 冒泡属性

5. beginWork(ul)
   - 协调children(3个li)
   - 执行Diff算法

Diff过程:
旧:[li(1), li(2), li(3)]
新:[li(1), li(2), li(3)]

第一轮遍历:
- li(1) vs li(1):key相同,type相同,复用
- li(2) vs li(2):key相同,type相同,复用(props变化,标记Update)
- li(3) vs li(3):key相同,type相同,复用

结果:
- li(1):复用,不更新
- li(2):复用,更新(checkbox的checked属性变化)
- li(3):复用,不更新

6. beginWork(li(1))
   - props没变,bailout

7. beginWork(li(2))
   - props变化,需要更新
   - 协调children(input, span, button)

8. beginWork(input)
   - checked属性变化
   - 标记Update

9. completeWork(input)
   - 计算updatePayload: ['checked', true]

10. completeWork(li(2))
    - 冒泡属性

11. completeWork(ul)
    - 冒泡属性

commit阶段:
1. mutation阶段
   - 更新input的checked属性

2. layout阶段
   - 执行useEffect(如果有)

场景2:删除第2项

用户点击第2项的删除按钮

render阶段Diff过程:
旧:[li(1), li(2), li(3)]
新:[li(1), li(3)]

第一轮遍历:
- li(1) vs li(1):key相同,复用
- li(2) vs li(3):key不同,跳出

第三轮遍历:
1. 创建Map:{ 2: li(2), 3: li(3) }
2. 处理li(3):从Map复用,oldIndex=2, lastPlacedIndex=0,不移动
3. 删除Map中剩余的li(2)

结果:
- li(1):复用,不移动
- li(2):删除(标记Deletion)
- li(3):复用,不移动

commit阶段:
1. mutation阶段
   - 从DOM中移除li(2)

场景3:重排序

用户点击重排序按钮

render阶段Diff过程:
旧:[li(1), li(2), li(3)]
新:[li(3), li(1), li(2)]

第一轮遍历:
- li(1) vs li(3):key不同,跳出

第三轮遍历:
1. 创建Map:{ 1: li(1), 2: li(2), 3: li(3) }

2. 处理li(3):
   - 从Map复用
   - oldIndex=2, lastPlacedIndex=0
   - 2 >= 0,不移动
   - lastPlacedIndex = 2

3. 处理li(1):
   - 从Map复用
   - oldIndex=0, lastPlacedIndex=2
   - 0 < 2,需要移动(标记Placement)
   - lastPlacedIndex = 2

4. 处理li(2):
   - 从Map复用
   - oldIndex=1, lastPlacedIndex=2
   - 1 < 2,需要移动(标记Placement)
   - lastPlacedIndex = 2

结果:
- li(3):复用,不移动
- li(1):复用,移动
- li(2):复用,移动

commit阶段:
1. mutation阶段
   - 将li(1)移动到li(3)后面
   - 将li(2)移动到li(1)后面

最终DOM顺序:li(3), li(1), li(2)

可视化:完整的协调过程


本章小结

本章深入讲解了React的协调过程,这是React更新机制的核心。让我们回顾一下关键要点:

render阶段的两个子阶段

  1. beginWork阶段:向下遍历Fiber树

    • 决定是否可以复用(bailout)
    • 执行组件逻辑
    • 执行Diff算法
    • 标记副作用flags
  2. completeWork阶段:向上归并Fiber树

    • 创建或更新DOM节点
    • 处理DOM属性
    • 冒泡childLanes和subtreeFlags

Diff算法的核心思想

  1. 单节点Diff:比较key和type,决定是否复用
  2. 多节点Diff:三轮遍历
    • 第一轮:处理更新的节点
    • 第二轮:处理剩余情况(删除或创建)
    • 第三轮:处理移动的节点
  3. key的重要性:帮助React识别节点,提高复用率

性能优化

  1. bailout机制:跳过没有变化的组件及其子树
  2. childLanes:快速判断子树是否有工作
  3. lastPlacedIndex:减少DOM移动次数

思考题

  1. 为什么React的Diff算法只比较同层节点,而不是整个树?
  2. 使用索引作为key在什么情况下是安全的?
  3. 如果一个组件的props没变,但context变了,会bailout吗?
  4. completeWork阶段创建的DOM节点什么时候插入到页面?

在下一章中,我们将学习Hooks的实现原理,理解useState、useEffect等Hook是如何工作的。