面试题 Interview Question
实现一个简化版的 Virtual DOM diff 函数
diff
,用于比较两个 Virtual DOM 树(以对象表示)并返回它们之间的差异(patches)
Implement a simplified Virtual DOM diff functiondiff
that compares two Virtual DOM trees (represented as JavaScript objects) and returns the differences (patches) between them.
要求 Requirements
-
diff(oldTree, newTree)
接收两个参数,分别代表旧的 Virtual DOM 树和新的 Virtual DOM 树(均为嵌套对象结构)。
diff(oldTree, newTree)
takes two parameters representing the old Virtual DOM tree and the new Virtual DOM tree, both as nested JavaScript object structures. -
Virtual DOM 树节点格式示例(示意):
The Virtual DOM tree node format example (for illustration):{ type: 'div', props: { id: 'container' }, children: [ { type: 'h1', props: {}, children: ['Title'] }, { type: 'p', props: {}, children: ['Content'] } ] }
每个节点包含
type
(标签名或文本标识)、props
(属性对象)、以及children
(子节点数组或文本) Each node contains atype
(tag name or text identifier),props
(an object of attributes), andchildren
(an array of child nodes or text). -
比较两个节点时需考虑以下三种变化类型:
When comparing two nodes, the following three types of changes need to be considered:- 节点类型不同:如果
oldNode.type !== newNode.type
,则生成替换整个节点的 patch。
Different node types: IfoldNode.type !== newNode.type
, generate a patch to replace the entire node. - 属性变化:如果相同类型节点的
props
对象不同,生成更新属性的 patch,包括新增、删除或修改属性。
Props changes: If the same-type nodes have differentprops
objects, generate attribute-update patches (add, remove, or update props). - 子节点变化:递归比较子数组,根据索引定位,生成子节点 diff,对应插入、删除或修改操作。
Children changes: Recursively compare thechildren
arrays by index to generate diff for insertion, deletion, or modification of child nodes.
- 节点类型不同:如果
-
返回值为一个对象
patches
,其键为节点在树中的唯一路径标识(如"0"
,"0.1"
,"0.1.2"
),值为对应位置的操作列表(例如{ type: 'REPLACE', node: newNode }
,{ type: 'PROPS', props: {...} }
,{ type: 'TEXT', text: 'Hello' }
)。
The return value should be an objectpatches
where keys are unique path identifiers in the tree (e.g.,"0"
,"0.1"
,"0.1.2"
), and values are lists of operations for that position (e.g.,{ type: 'REPLACE', node: newNode }
,{ type: 'PROPS', props: {...} }
,{ type: 'TEXT', text: 'Hello' }
). -
不要依赖第三方库(如 React、自定义哈希函数等),可直接操作 JavaScript 对象和数组。
Do not rely on third-party libraries (e.g., React or custom hashing functions); operate directly on plain JavaScript objects and arrays . -
代码应保持可读性,使用函数式实现。
The code should remain readable and be implemented in a functional style.
参考答案 Reference Solution
下面示例实现一个基本的 diff
函数,生成 patches
对象。
The following example implements a basic diff
function that generates a patches
object.
// 定义 patch 类型常量
const PATCH_TYPES = {
REPLACE: 'REPLACE', // 替换整个节点
PROPS: 'PROPS', // 更新属性
TEXT: 'TEXT', // 文本变更
REORDER: 'REORDER' // 子节点插入/删除
};
/**
* 计算两个对象浅比较后的属性差异
* @param {Object} oldProps 旧属性
* @param {Object} newProps 新属性
* @returns {Object} propPatches 包含要更新、删除或新增的属性
*/
function diffProps(oldProps = {}, newProps = {}) {
const patches = {};
// 查找要修改或删除的属性
Object.keys(oldProps).forEach(key => {
if (!newProps.hasOwnProperty(key)) {
patches[key] = null; // 属性被删除
} else if (oldProps[key] !== newProps[key]) {
patches[key] = newProps[key]; // 属性值被修改
}
});
// 查找要新增的属性
Object.keys(newProps).forEach(key => {
if (!oldProps.hasOwnProperty(key)) {
patches[key] = newProps[key]; // 属性被新增
}
});
return patches;
}
/**
* 递归比较两个 Virtual DOM 树生成 patch 集合
* @param {Object|string} oldNode 旧节点(可能为文本或对象)
* @param {Object|string} newNode 新节点(可能为文本或对象)
* @param {string} path 当前节点在树中的路径标识,例如 ''、'0.1' 等
* @param {Object} patches 累积的 patch 对象
*/
function walk(oldNode, newNode, path, patches) {
const currentPatches = [];
// 1. 节点为 null 或 未定义,直接忽略
if (oldNode == null && newNode == null) {
return;
}
// 2. 类型不同时,替换整个节点
if (oldNode && newNode && oldNode.type !== newNode.type) {
currentPatches.push({ type: PATCH_TYPES.REPLACE, node: newNode });
}
// 3. 文本节点比较
else if (typeof oldNode === 'string' && typeof newNode === 'string') {
if (oldNode !== newNode) {
currentPatches.push({ type: PATCH_TYPES.TEXT, text: newNode });
}
}
// 4. 相同类型的元素节点,属性比较 & 子节点递归
else if (oldNode && newNode && oldNode.type === newNode.type) {
// 比较属性差异
const propDiffs = diffProps(oldNode.props, newNode.props);
if (Object.keys(propDiffs).length > 0) {
currentPatches.push({ type: PATCH_TYPES.PROPS, props: propDiffs });
}
// 递归比较子节点
diffChildren(oldNode.children || [], newNode.children || [], path, patches, currentPatches);
}
// 如果当前节点有任何 patch,则将其存入 patches 对象
if (currentPatches.length > 0) {
patches[path] = currentPatches;
}
}
/**
* 比较两个子节点列表,递归调用 walk,生成索引路径的 patches
* @param {Array} oldChildren 旧子节点数组
* @param {Array} newChildren 新子节点数组
* @param {string} path 父节点路径
* @param {Object} patches 累积的 patch 对象
* @param {Array} currentPatches 当前节点的补丁列表,可能包含子节点 reorder 操作
*/
function diffChildren(oldChildren, newChildren, path, patches, currentPatches) {
const maxLen = Math.max(oldChildren.length, newChildren.length);
// 遍历所有可能的索引
for (let i = 0; i < maxLen; i++) {
const oldChild = oldChildren[i];
const newChild = newChildren[i];
const currentPath = path ? `${path}.${i}` : `${i}`;
if (oldChild && newChild) {
// 递归比较同索引子节点
walk(oldChild, newChild, currentPath, patches);
} else if (newChild && !oldChild) {
// 插入新节点
currentPatches.push({ type: PATCH_TYPES.REORDER, insert: { index: i, node: newChild } });
} else if (oldChild && !newChild) {
// 删除旧节点
currentPatches.push({ type: PATCH_TYPES.REORDER, remove: { index: i } });
}
}
}
/**
* diff 函数入口,用于初始化 patch 集合并调用 walk
* @param {Object|string} oldTree 旧 Virtual DOM 树
* @param {Object|string} newTree 新 Virtual DOM 树
* @returns {Object} patches 包含每个路径上的操作列表
*/
function diff(oldTree, newTree) {
const patches = {};
walk(oldTree, newTree, '', patches);
return patches;
}
解释 Explanation
-
PATCH_TYPES
定义了四种补丁类型:
PATCH_TYPES
defines four types of patches:REPLACE
:替换整个节点(包括元素节点或文本)。
REPLACE
: Replaces the entire node (either an element or a text node).PROPS
:更新属性,包括新增、删除或修改属性。
PROPS
: Updates properties, including adding, deleting, or modifying attributes.TEXT
:文本节点内容变化。
TEXT
: Indicates changes in the content of a text node.REORDER
:子节点插入或删除(重排操作)。
REORDER
: Represents the insertion or deletion (reordering) of child nodes.
-
diffProps
函数通过浅比较两个属性对象,产生一个差异映射,值为null
表示删除该属性。
ThediffProps
function performs a shallow comparison between two property objects and produces a difference map. A value ofnull
indicates that the property should be removed. -
walk
函数是递归核心:
Thewalk
function is the core of the recursion:- 若节点类型不同,生成
REPLACE
补丁;若都是字符串且内容不同,生成TEXT
补丁。
If node types differ, aREPLACE
patch is generated. If both are strings but their content differs, aTEXT
patch is generated. - 若节点类型相同且为元素节点,则比较属性差异并递归比较子节点。
If node types are the same and both are element nodes, it compares property differences and recursively compares child nodes. - 每次对比后,如
currentPatches
非空,则以路径path
为键将补丁列表写入patches
对象。
After each comparison, ifcurrentPatches
is not empty, the patch list is written into thepatches
object using the currentpath
as the key.
- 若节点类型不同,生成
-
diffChildren
依次比较旧子节点与新子节点:
diffChildren
compares old and new child nodes sequentially:- 如果两者同在,则递归调用
walk
。
If both exist, it recursively callswalk
. - 如果新子节点存在而旧子节点已被删除,则生成
REORDER
插入补丁。
If the new child exists but the old one does not, aREORDER
insert patch is generated. - 如果旧子节点存在而新子节点不存在,则生成
REORDER
删除补丁。
If the old child exists but the new one does not, aREORDER
remove patch is generated.
- 如果两者同在,则递归调用
-
初次调用
diff(oldTree, newTree)
时,path
传空字符串''
,表示根节点;后续子节点路径以"0"
,"0.1"
,"0.1.2"
等形式标识,以便调用者在实际 DOM 上执行对应操作。
When initially callingdiff(oldTree, newTree)
, thepath
is passed as an empty string''
, representing the root node. Subsequent child node paths are identified as"0"
,"0.1"
,"0.1.2"
, etc., allowing the caller to apply corresponding operations on the actual DOM.
示例 Example
以下示例展示如何使用 diff
函数比较两棵简单的 Virtual DOM 树,并查看结果:
The following example demonstrates how to use the diff
function to compare two simple Virtual DOM trees and inspect the result:
// 旧树 (oldTree)
const oldTree = {
type: 'div',
props: { id: 'container' },
children: [
{ type: 'h1', props: {}, children: ['Hello'] },
{ type: 'p', props: {}, children: ['World'] }
]
};
// 新树 (newTree):将 h1 文本改为 Hi,并新增一个 span
const newTree = {
type: 'div',
props: { id: 'container', className: 'main' }, // 新增 className
children: [
{ type: 'h1', props: {}, children: ['Hi'] }, // 文本变更
{ type: 'p', props: {}, children: ['World'] },
{ type: 'span', props: {}, children: ['!'] } // 新增节点
]
};
// 计算差异
const patches = diff(oldTree, newTree);
console.log(JSON.stringify(patches, null, 2));
/*
预期输出 (示例):
{
"": [
{ "type": "PROPS", "props": { "className": "main" } }
],
"0": [
{ "type": "TEXT", "text": "Hi" }
],
"2": [
{ "type": "REORDER", "insert": { "index": 2, "node": { type: "span", props: {}, children: ["!"] } } }
]
}
*/
- 在根路径
""
,属性从{ id: "container" }
变为{ id: "container", className: "main" }
,生成PROPS
补丁
At the root path""
, the properties change from{ id: "container" }
to{ id: "container", className: "main" }
, generating aPROPS
patch. - 在路径
"0"
(对应第一个子节点 h1),文本从"Hello"
变为"Hi"
,生成TEXT
补丁
At path"0"
(corresponding to the first child nodeh1
), the text changes from"Hello"
to"Hi"
, generating aTEXT
patch. - 在路径
"2"
(新插入的第三个子节点),旧树没有该节点,生成REORDER
插入补丁
At path"2"
(newly inserted third child node), the old tree does not have this node, so aREORDER
insert patch is generated.
面试考察点 Interview Focus
- Virtual DOM 原理:理解 Virtual DOM 在 React 中的作用,包括如何通过内存中的 JS 对象树比对并最小化更新实际 DOM .
Principle of the Virtual DOM: Understand the role of the Virtual DOM in React, including how it compares trees in memory using JavaScript objects and minimizes updates to the real DOM. - Diff 算法思想:掌握基本的树形结构对比思路,包括节点类型判断、属性对比、文本节点处理和子节点递归等 .
Diff Algorithm Concepts: Master the basic ideas of comparing tree structures, such as node type checks, property comparison, text node handling, and recursive traversal of child nodes. - Patch 数据结构设计:设计能准确描述各种变更(替换、属性变更、文本更新、重排)的补丁格式,以便后续直接应用到真实 DOM 上 .
Patch Data Structure Design: Design patch formats that accurately describe changes (replacement, property updates, text changes, and reordering) so they can be directly applied to the real DOM. - JavaScript 对象与数组操作:熟练使用 JS 对象浅比较 (
diffProps
),以及基于索引的数组遍历实现子节点对比 .
JavaScript Object & Array Manipulation: Be proficient in shallow comparison of JS objects (diffProps
) and array traversal based on indexes to compare child nodes. - 可扩展性与性能考量:讨论如何提升 diff 性能(如 keyed diff、增量渲染)、如何处理大规模树或动态增删节点,以及如何生成最小补丁集以避免多余操作 .
Scalability & Performance Considerations: Discuss how to improve diff performance (e.g., keyed diff, incremental rendering), handle large trees or dynamic node additions/deletions, and generate minimal patch sets to avoid redundant operations. - 错误处理与边界情况:处理
null
或undefined
节点、非数组children
、节点类型异常等情况,确保函数鲁棒性;必要时进行类型检查,以免遍历空值导致运行时错误。
Error Handling & Edge Cases: Handlenull
orundefined
nodes, non-arraychildren
, and abnormal node types to ensure function robustness. Include type checks when necessary to prevent runtime errors during traversal. - 实际 DOM 更新:面试官可进一步询问如何将
patches
应用到真实的 DOM(例如使用document.createElement
、replaceChild
、setAttribute
、removeChild
等原生 API)。
Real DOM Updates: Interviewers may further ask how to applypatches
to the real DOM (e.g., using native APIs likedocument.createElement
,replaceChild
,setAttribute
, andremoveChild
). - 高级优化与框架启示:了解 React Fiber、调度优先级、分片更新等高级主题;讨论为何框架选用分层 diff 策略以保证用户交互流畅 .
Advanced Optimizations & Framework Insights: Learn about advanced topics such as React Fiber, scheduling priorities, and concurrent rendering. Discuss why frameworks adopt layered diff strategies to ensure smooth user interactions.