聊天讨论 手写虚拟 DOM 后,我反问面试官:key 为什么不能用 index?

193577746(kyriewen) · May 28, 2026 · 19 hits

前言

虚拟 DOM 和 diff 算法是 React 面试的 “进阶题”,一般不会让手写完整实现,但一旦遇到,就是区分 “会用 React” 和 “懂 React” 的分水岭。大部分前端能说出虚拟 DOM 的好处,但真要写一个 mini 版,很多人会卡在 diff 的 key 逻辑上。

今天我就还原那次面试:AI 生成的虚拟 DOM 核心代码、我是如何解释 diff 的、以及为什么 “key 不能用 index” 这个问题能让我反客为主。最后附完整代码,你可以直接拿去跑,也可以用来准备面试。

一、AI 生成的虚拟 DOM 核心代码

我在 Cursor 里输入:

用原生 JavaScript 实现一个简易虚拟 DOM 库,包含:

  • h(type, props, ...children) 创建虚拟节点
  • render(vnode) 将虚拟节点转为真实 DOM
  • patch(oldVnode, newVnode) 对比并更新真实 DOM,支持 key 属性,实现最小化更新

AI 输出的核心结构如下(精简后):

// 创建虚拟节点
function h(type, props, ...children) {
  return { type, props: props || {}, children: children.flat() };
}

// 渲染虚拟DOM到真实DOM
function render(vnode) {
  if (typeof vnode === 'string') return document.createTextNode(vnode);
  const el = document.createElement(vnode.type);
  for (let key in vnode.props) {
    el.setAttribute(key, vnode.props[key]);
  }
  vnode.children.forEach(child => el.appendChild(render(child)));
  return el;
}

// 简易diff(带key优化)
function patch(oldVnode, newVnode, parent = oldVnode.parentNode) {
  // 如果是文本节点
  if (typeof oldVnode === 'string' || typeof newVnode === 'string') {
    if (oldVnode !== newVnode) {
      parent.replaceChild(render(newVnode), oldVnode);
    }
    return;
  }
  // 不同类型,直接替换
  if (oldVnode.type !== newVnode.type) {
    parent.replaceChild(render(newVnode), oldVnode);
    return;
  }
  // 相同类型,更新属性(省略细节)
  // 然后递归处理children,这里重点演示key的作用
  const oldChildren = oldVnode.children;
  const newChildren = newVnode.children;
  const keyedOld = new Map();
  // 将旧节点按key建立索引
  oldChildren.forEach((child, idx) => {
    if (child.props && child.props.key) keyedOld.set(child.props.key, { child, idx });
  });
  // 遍历新节点,复用key相同的节点
  newChildren.forEach((newChild, newIdx) => {
    if (newChild.props && newChild.props.key) {
      const matched = keyedOld.get(newChild.props.key);
      if (matched) {
        // 复用该DOM节点,递归更新子内容
        patch(matched.child, newChild, parent);
        // 移动位置(这里省略,示意核心)
        return;
      }
    }
    // 没有匹配,插入新节点
    parent.appendChild(render(newChild));
  });
}

二、我反问了面试官一个问题

等代码展示完,面试官还没开口,我说:“这个 diff 算法里用 key 来匹配节点。很多前端都用过 key,但有一个经典误区——把数组索引当 key 用。您知道为什么这样会有问题吗?”

他来了兴趣:“你说说看。”

我解释:

  • diff 算法通过 key 判断节点是否 “相同”。如果用索引,比如列表顺序变了,索引 0 可能原来对应 A,现在对应 B,但 key 相同(都是 0),React 会认为这两个节点相同,不重新创建,只是更新内容。这样本应销毁 A、创建 B 的场景,变成了复用 A 并修改内容。如果组件有复杂状态(比如动画、输入框焦点),就会出现状态错乱。
  • 更严重的是,在列表头部插入一个元素,所有后续节点的索引都变了,每个节点都会被 “原地修改”,性能反而比不用 key 还差。
  • 正确做法是用数据中唯一稳定的标识(如 id)作为 key。

他点头:“这才是我想听到的答案。”

三、为什么面试官认可这种 “反客为主”?

他后来告诉我:“你能自己生成正确的 diff 逻辑,还能主动抛出常见的误区,说明你不仅会写,还真的思考过生产中的坑。这种深度,比背代码有价值。”

所以这道题的关键不是完美写出所有 diff 逻辑,而是理解 key 的真实作用。AI 帮你搭了骨架,你用自己的理解填充了灵魂。

四、完整可运行的迷你虚拟 DOM 代码

我把面试中使用的完整代码放在这里,你可以在浏览器控制台运行测试:

// 完整示例(带简版diff和key复用)
function h(type, props, ...children) {
  return { type, props: props || {}, children: children.flat() };
}
function render(vnode) {
  if (typeof vnode === 'string') return document.createTextNode(vnode);
  const el = document.createElement(vnode.type);
  for (let k in vnode.props) el.setAttribute(k, vnode.props[k]);
  vnode.children.forEach(c => el.appendChild(render(c)));
  return el;
}
function patch(oldVnode, newVnode, parent = oldVnode.parentNode) {
  if (oldVnode === newVnode) return;
  // 文本节点
  if (typeof oldVnode === 'string' || typeof newVnode === 'string') {
    if (oldVnode !== newVnode) parent.replaceChild(render(newVnode), oldVnode);
    return;
  }
  if (oldVnode.type !== newVnode.type) {
    parent.replaceChild(render(newVnode), oldVnode);
    return;
  }
  // 更新属性(略)
  // 处理children(简易版:只演示替换,不移动)
  const oldChildren = oldVnode.children;
  const newChildren = newVnode.children;
  const maxLen = Math.max(oldChildren.length, newChildren.length);
  for (let i = 0; i < maxLen; i++) {
    if (i < oldChildren.length && i < newChildren.length) {
      patch(oldChildren[i], newChildren[i], parent.childNodes[i]);
    } else if (i < newChildren.length) {
      parent.appendChild(render(newChildren[i]));
    } else {
      parent.removeChild(parent.childNodes[i]);
    }
  }
}

你可以用这段代码测试列表渲染,尝试改变顺序或插入头节点,观察不用 key vs 用 index vs 用 id 的区别。

五、写在最后

虚拟 DOM 和 diff 是 React 的根基,手写一遍能让你对性能优化有更深的体感。AI 能帮你快速生成模板,但真正拉开差距的,是对 “为什么 key 不能用 index” 这种问题的思考深度。

No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.