面试官:React Diff 算法原理?我:三个假设 + O(n) 复杂度,手撕给你看!

213 阅读5分钟

👨‍🏫 本系列由前端面试真题博主 Kincy 发起,每日更新一题,通勤路上轻松掌握高频知识点。
📢 如果你想第一时间获取更新,或与群友交流面试经验、内推信息,欢迎加入微信群(文末扫码)!

🧠 系列前言:

面试题千千万,我来帮你挑重点。每天一道,通勤路上、蹲坑时、摸鱼中,技术成长不设限!本系列主打幽默 + 深度 + 面霸必备语录,你只管看,面试场上稳拿 offer!

💬 面试官发问:

“React 的 Virtual DOM diff 算法是怎么实现的?为啥非得用 key?三个假设具体是啥?”

啊哈!经典中的经典,这题出现在面试题中就像胡辣汤出现在早餐摊上——朴实无华,但谁都得喝!

🎯 快答区(面霸速记版)

React 的 diff 算法是一套启发式策略,核心基于三个假设:

  1. 同层级比较,不跨层比对
  2. 不同类型的节点,直接替换
  3. 列表 diff 使用 key 追踪元素身份

这样,React 就能把传统 diff 算法的复杂度从天文数字级别(比如 O(n³))优化成了大多数情况下的 O(n) ,性能又快又稳,堪称算法届“职场卷王”。

🎬 React Diff 算法的江湖传说

🥷 一、为啥 React 要搞 diff?

想象你是个居家好男人,帮对象写购物清单:

// 上周买的
const oldList = ['白菜', '土豆', '西红柿', '鸡蛋']
// 这周对象改了点
const newList = ['白菜', '西兰花', '西红柿', '鸡蛋']

你总不能每次都把整个超市搬回家吧?只需要替换掉那个“土豆”就好。这就是 diff 的意义——找出变化的最小集合,避免无脑重建

📚 二、传统算法的噩梦:O(n³)

理论上最完整的 diff,要比较两棵树的所有可能变换路径,这叫 Tree Edit Distance,时间复杂度 O(n³)。这放在网页里?那用户等页面加载的时候,AI 都能写完一篇小说了。

于是 React 聪明地说:

咱不和你玩复杂度游戏,咱搞点“工程上的小聪明”。

🧠 三、React 的三个小聪明(启发式假设)

🪜 假设 1:同层级比较就够了

// 很少有场景这么干
<div>
  <span>Hi</span>
</div>
<p>
  <span>Hi</span>
</p>

React:你要是从 <div> 里搬出 <span><p> 里,我懒得追踪,直接砍了重建!

🧬 假设 2:不同类型,直接换掉

<div>Hello</div> → <section>Hello</section>

React:看名字都不一样,我不废话,直接删旧建新。组件切换?直接卸载挂载。

🧷 假设 3:key 是元素的身份证

{items.map(item => <Item key={item.id} />)}

React:有了 key,我就知道你是谁、从哪来、要去哪。没 key?我只能靠“位置”猜测你是谁,小心猜错你就变成“夺舍现场”。

⚔️ 四、Diff 的三大战场

1️⃣ Tree Diff(DOM 节点层)

  • 类型一样 → 比较属性和子节点
  • 类型不一样 → 删除旧节点,挂载新节点

2️⃣ Component Diff(组件层)

  • 同类型组件 → 更新 props,走生命周期
  • 不同类型组件 → 卸载旧的,挂载新的(清理 effect、生命周期都触发)

3️⃣ Element Diff(列表层)🔥

const old = ['A', 'B', 'C']
const new_ = ['B', 'A', 'C']

没有 key 的时候 React 会这样想:

  • B 是 A?可能不是,先删再建吧
  • A 是 B?唔,好像也不是……
  • C 没变,放过它

结果:两次删除 + 两次创建,一顿操作猛如虎,页面跳动像海豚

有了 key,React 心中有数:

“哦,你们只是调换了下位置,那我只需要移动 DOM,不需要删。”

💥 五、key 到底多重要?

❌ 错误示范

{list.map((item, index) => (
  <li key={index}>{item}</li>
))}

你改一下排序,DOM 大换血。更离谱的是,组件状态还跟着乱跳。

✅ 正确示范

{list.map(item => (
  <li key={item.id}>{item.name}</li>
))}

这才是文明开发者的打开方式。key 是稳定唯一的,React 的 diff 才不会疯。

🧩 六、React Diff 的源码内幕(简化版)

React 的 Diff 实际在执行 Fiber 架构,它不是递归函数式 diff,而是构建链表结构的 Fiber 树,并通过以下逻辑控制更新:

function reconcileChildFibers(
  returnFiber,
  currentFirstChild,
  newChild
) {
  if (typeof newChild === 'object' && newChild !== null) {
    if (Array.isArray(newChild)) {
      return reconcileChildrenArray(returnFiber, currentFirstChild, newChild)
    }
    return reconcileSingleElement(returnFiber, currentFirstChild, newChild)
  }
}

reconcileChildrenArray() 核心逻辑:

// 简化逻辑
const existingChildren = mapRemainingChildren(currentFirstChild) // 用 Map 存旧节点
for (let i = 0; i < newChildren.length; i++) {
  const newChild = newChildren[i]
  const key = newChild.key || i
  const matchedFiber = existingChildren.get(key)

  if (matchedFiber) {
    const newFiber = useFiber(matchedFiber, newChild.props)
    existingChildren.delete(key)
  } else {
    const newFiber = createFiberFromElement(newChild)
  }
}

📉 七、React ≠ 双指针算法!

有些文章会说 React 用“双指针算法”处理列表 diff,这是误传。

  • 那是 Vue 的实现方式(通过头尾指针来移动节点)
  • React 实际是:先顺序尝试复用 → 构建 key map → 再找匹配 → 标记移动或删除

别搞混了!

🎓 八、装 X 语录(限时使用)

“React 的 diff 算法基于启发式策略,不追求最优解,而是用工程上足够好的 O(n) 实现保证性能。”

“key 是 React 识别组件身份的身份证,用错了就像身份证错换,小心状态串台。”

“Fiber 架构下的 diff 实际是增量构建一棵新树,而不是树和树的递归比对,这点和 Vue 完全不同。”

说完记得喝口水,微笑看着面试官点头 🤝

✅ 总结一句话

React Diff = 启发式、线性复杂度、靠 key 重用、Fiber 架构支撑下的最小变更生成系统
不是最聪明的算法,但是最适合前端性能和维护的方案之一。

🔚 明日预告

明天我们聊聊 useEffect 为何总能背刺你,它和闭包、依赖数组之间到底藏着什么“爱恨情仇”🪝。

📌 点赞 + 收藏 + 关注系列,React 面试不再抓瞎!

📚 本系列每天一题,持续更新中!
👉 扫码加入【前端面试题交流群】,一起成长、交流、内推、分享机会!

微信二维码.png 备注“掘金”优先通过