返回首页

说说对 React 的 diff 算法的理解?

问题解析(面试官考察点)

面试官通过此问题主要考察:

  • 对 React Diff 算法核心思想的理解
  • 对 Virtual DOM 与真实 DOM 对比机制的了解
  • 对 Diff 算法三个层级策略的掌握
  • 对 React 性能优化原理的认识

核心概念(基础知识点)

什么是 Diff 算法

Diff 算法是 React 用于比较两棵 Virtual DOM 树(新旧树)差异的算法。它的核心目标是以最小的代价找出需要更新的部分,从而最小化对真实 DOM 的操作。

Diff 算法的核心思想

  1. O(n) 复杂度: 将对比复杂度从 O(n³) 降低到 O(n)
  2. 分层比较: 只比较同层节点,不跨层比较
  3. 类型比较: 不同类型的元素直接替换
  4. Key 优化: 使用 key 优化列表对比

详细解答(代码示例)

Diff 算法的三个层级

┌─────────────────────────────────────────────────────────────┐
│                   React Diff 三个层级                        │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. Tree Diff(树层级)                                      │
│     - 逐层对比                                               │
│     - 跨层节点直接删除/创建                                  │
│                                                              │
│  2. Component Diff(组件层级)                               │
│     - 不同类型组件直接替换                                   │
│     - 同类型组件对比 props 和 state                          │
│                                                              │
│  3. Element Diff(元素层级)                                 │
│     - 使用 key 优化列表对比                                  │
│     - 三种操作:插入、移动、删除                             │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Tree Diff(树层级对比)

// Tree Diff 策略:逐层对比,跨层操作直接删除/创建

// 旧树
const oldTree = (
  <div>
    <p>Hello</p>
    <span>World</span>
  </div>
);

// 新树 - 跨层移动(不推荐,性能差)
const newTree = (
  <div>
    <section>
      <p>Hello</p>
    </section>
    <span>World</span>
  </div>
);

// React 的处理:
// 1. 发现 div 的子元素从 [p, span] 变为 [section, span]
// 2. p 元素被移动到 section 内部(跨层)
// 3. React 不会尝试移动,而是:
//    - 删除原来的 p
//    - 创建新的 section
//    - 在 section 中创建新的 p
// 4. span 保持不变

// 性能建议:避免跨层移动节点

Component Diff(组件层级对比)

// Component Diff 策略:不同类型直接替换

// 场景1:相同类型的组件
class Counter extends React.Component {
  render() {
    return <div>{this.props.count}</div>;
  }
}

// 旧:<Counter count={0} />
// 新:<Counter count={1} />
// React 会保留组件实例,只更新 props

// 场景2:不同类型的组件
// 旧:<Counter count={0} />
// 新:<div>0</div>
// React 会卸载 Counter 组件,创建新的 div 元素

// 场景3:自定义组件 vs 原生元素
function Demo() {
  const [useComponent, setUseComponent] = useState(true);

  return (
    <div>
      {useComponent ? (
        <MyComponent />  // 自定义组件
      ) : (
        <div>原生元素</div>  // 原生元素
      )}
    </div>
  );
}
// 切换时,MyComponent 会被完全卸载,div 会被重新创建

Element Diff(元素层级对比)

// Element Diff 策略:使用 key 优化列表对比

// 场景1:列表末尾插入(最优情况)
// 旧:[A, B, C]
// 新:[A, B, C, D]
// React:直接在末尾插入 D

// 场景2:列表开头插入(使用 key 优化)
// 旧:[A(key=a), B(key=b), C(key=c)]
// 新:[D(key=d), A(key=a), B(key=b), C(key=c)]
// React:识别出 A、B、C 只是后移,插入 D 到开头

// 场景3:没有 key 的问题
// 旧:[A, B, C](使用索引作为 key:0, 1, 2)
// 新:[D, A, B, C](使用索引作为 key:0, 1, 2, 3)
// React:认为所有元素都变了,全部更新(性能差)

// 正确的列表渲染
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Diff 算法源码简化

// React Diff 算法简化示意

// 1. 单节点对比
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
  const key = element.key;
  const child = currentFirstChild;

  // 尝试找到匹配的子节点
  while (child !== null) {
    if (child.key === key) {
      if (child.type === element.type) {
        // 找到匹配的节点,复用
        const existing = useFiber(child, element.props);
        existing.return = returnFiber;
        return existing;
      } else {
        // key 相同但类型不同,删除旧节点
        deleteChild(returnFiber, child);
      }
    } else {
      // key 不同,删除旧节点
      deleteChild(returnFiber, child);
    }
    child = child.sibling;
  }

  // 创建新节点
  const created = createFiberFromElement(element, returnFiber.mode);
  created.return = returnFiber;
  return created;
}

// 2. 多节点对比(列表)
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  let resultingFirstChild = null;
  let previousNewFiber = null;
  let oldFiber = currentFirstChild;
  let lastPlacedIndex = 0;
  let newIdx = 0;

  // 第一轮:按位置对比
  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]);

    if (newFiber === null) {
      // key 不同,跳出循环
      break;
    }

    if (oldFiber && newFiber.alternate === null) {
      // 删除旧节点
      deleteChild(returnFiber, oldFiber);
    }

    // 记录放置位置
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }

  // 第二轮:处理剩余的新元素或删除旧元素
  if (newIdx === newChildren.length) {
    // 新元素处理完毕,删除剩余旧元素
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  // 第三轮:处理移动(基于 key 的 map)
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx]
    );

    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) {
    existingChildren.forEach(child => deleteChild(returnFiber, child));
  }

  return resultingFirstChild;
}

深入理解(原理剖析)

Diff 算法的复杂度优化

┌─────────────────────────────────────────────────────────────┐
│                Diff 算法复杂度对比                           │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  传统算法(树编辑距离)                                       │
│  - 需要找出两棵树之间的最小转换操作                           │
│  - 时间复杂度:O(n³)                                         │
│  - 1000 个节点需要 10 亿次比较                               │
│                                                              │
│  React Diff 算法                                             │
│  - 基于三个策略的启发式算法                                   │
│  - 时间复杂度:O(n)                                          │
│  - 1000 个节点只需要 1000 次比较                             │
│                                                              │
│  优化策略:                                                   │
│  1. Web UI 中跨层移动节点很少                                 │
│  2. 不同类型的组件会产生不同的树结构                          │
│  3. 可以通过 key 提示哪些子元素是稳定的                       │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Diff 算法的执行流程

┌─────────────────────────────────────────────────────────────┐
│                   Diff 算法执行流程                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. 开始对比                                                  │
│     currentFiber vs workInProgress                           │
│                                                              │
│  2. 判断节点类型                                              │
│     ├─ 相同类型 → 复用节点(更新 props)                     │
│     └─ 不同类型 → 删除旧节点,创建新节点                     │
│                                                              │
│  3. 处理子节点                                                │
│     ├─ 单个子节点 → 直接对比                                 │
│     ├─ 多个子节点(列表)→ 使用 key 优化对比                 │
│     └─ 文本节点 → 对比文本内容                               │
│                                                              │
│  4. 标记副作用                                                │
│     ├─ Placement(插入)                                     │
│     ├─ Update(更新)                                        │
│     ├─ Deletion(删除)                                      │
│     └─ PlacementAndUpdate(移动并更新)                      │
│                                                              │
│  5. 继续遍历子树                                              │
│                                                              │
└─────────────────────────────────────────────────────────────┘

双缓冲技术(Double Buffering)

// React 使用双缓冲技术优化渲染

// current Fiber Tree(当前显示在屏幕上的树)
const current = {
  type: 'div',
  props: { children: 'Hello' },
  stateNode: domNode,  // 对应的真实 DOM
  alternate: workInProgress  // 指向 workInProgress
};

// workInProgress Fiber Tree(正在构建的树)
const workInProgress = {
  type: 'div',
  props: { children: 'World' },
  stateNode: null,  // 还未创建真实 DOM
  alternate: current  // 指向 current
};

// Diff 过程:
// 1. 基于 current 创建 workInProgress
// 2. 在 workInProgress 上进行更新
// 3. 更新完成后,交换两者:current = workInProgress
// 4. workInProgress 变为下一次更新的 current

最佳实践

1. 使用 Key 优化列表

// 推荐:使用稳定的唯一标识作为 key
function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// 避免:使用索引作为 key(列表变化时性能差)
function BadUserList({ users }) {
  return (
    <ul>
      {users.map((user, index) => (
        <li key={index}>{user.name}</li>
      ))}
    </ul>
  );
}

2. 避免跨层移动节点

// 不好的做法:条件渲染改变层级
function BadExample({ showDetail }) {
  return (
    <div>
      {showDetail ? (
        <DetailWrapper>
          <Header />
          <Content />
        </DetailWrapper>
      ) : (
        <>
          <Header />
          <Content />
        </>
      )}
    </div>
  );
}
// 切换时 Header 和 Content 会被销毁重建

// 好的做法:保持层级稳定
function GoodExample({ showDetail }) {
  return (
    <div>
      <Header />
      <Content />
      {showDetail && <Detail />}
    </div>
  );
}

3. 组件类型保持一致

// 不好的做法:切换组件类型
function BadExample({ useList }) {
  return (
    <div>
      {useList ? (
        <ListView items={items} />
      ) : (
        <GridView items={items} />
      )}
    </div>
  );
}

// 好的做法:使用相同的包装组件
function GoodExample({ useList }) {
  return (
    <div>
      <ViewContainer layout={useList ? 'list' : 'grid'}>
        {items.map(item => <Item key={item.id} {...item} />)}
      </ViewContainer>
    </div>
  );
}

4. 使用 React.memo 减少不必要的 Diff

// 使用 React.memo 避免不必要的对比
const ExpensiveComponent = React.memo(({ data }) => {
  // 复杂的渲染逻辑
  return <div>{/* ... */}</div>;
});

// 自定义比较函数
const CustomComponent = React.memo(
  ({ user }) => {
    return <div>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    // 只比较 name,忽略其他属性变化
    return prevProps.user.name === nextProps.user.name;
  }
);

5. 虚拟列表优化长列表

// 使用 react-window 优化长列表
import { FixedSizeList as List } from "react-window";

function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <List
      height={500}
      itemCount={items.length}
      itemSize={35}
      width="100%"
    >
      {Row}
    </List>
  );
}
// 只渲染可见区域的元素,大大减少 Diff 计算量

面试要点

  1. Diff 算法是什么: 比较新旧 Virtual DOM 树差异的算法,目标是最小化 DOM 操作

  2. 复杂度优化: 从 O(n³) 优化到 O(n),基于三个策略:

    • 跨层移动很少
    • 不同类型组件产生不同树
    • 使用 key 优化列表
  3. 三个层级:

    • Tree Diff:逐层对比
    • Component Diff:不同类型直接替换
    • Element Diff:使用 key 优化列表对比
  4. Key 的作用: 帮助 React 识别元素,优化列表对比性能

  5. 优化建议:

    • 使用稳定的 key
    • 避免跨层移动节点
    • 保持组件类型一致
    • 使用 React.memo
    • 长列表使用虚拟列表