
虚拟 DOM 和 diff 算法是 React 面试的 “进阶题”,一般不会让手写完整实现,但一旦遇到,就是区分 “会用 React” 和 “懂 React” 的分水岭。大部分前端能说出虚拟 DOM 的好处,但真要写一个 mini 版,很多人会卡在 diff 的 key 逻辑上。
今天我就还原那次面试:AI 生成的虚拟 DOM 核心代码、我是如何解释 diff 的、以及为什么 “key 不能用 index” 这个问题能让我反客为主。最后附完整代码,你可以直接拿去跑,也可以用来准备面试。
我在 Cursor 里输入:
用原生 JavaScript 实现一个简易虚拟 DOM 库,包含:
h(type, props, ...children)创建虚拟节点render(vnode)将虚拟节点转为真实 DOMpatch(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 逻辑,还能主动抛出常见的误区,说明你不仅会写,还真的思考过生产中的坑。这种深度,比背代码有价值。”
所以这道题的关键不是完美写出所有 diff 逻辑,而是理解 key 的真实作用。AI 帮你搭了骨架,你用自己的理解填充了灵魂。
我把面试中使用的完整代码放在这里,你可以在浏览器控制台运行测试:
// 完整示例(带简版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” 这种问题的思考深度。