数据对比场景下的前端实现方案与常见陷阱
先来个简单的数据对比工具类
最近项目里经常需要做数据对比,特别是用户修改配置后的差异展示,之前都是手动写一堆判断逻辑,太麻烦了。今天把我封装的一个数据对比工具类分享出来,亲测有效。
class DataComparator {
constructor() {
this.differences = [];
this.path = [];
}
compare(obj1, obj2) {
this.differences = [];
this._compareRecursive(obj1, obj2);
return this.differences;
}
_compareRecursive(val1, val2, path = []) {
// 类型不同直接标记为差异
if (typeof val1 !== typeof val2) {
this.differences.push({
path: path.join('.'),
oldVal: val1,
newVal: val2,
type: 'type_change'
});
return;
}
// 数组对比
if (Array.isArray(val1) && Array.isArray(val2)) {
this._compareArrays(val1, val2, path);
}
// 对象对比
else if (typeof val1 === 'object' && val1 !== null && val2 !== null) {
this._compareObjects(val1, val2, path);
}
// 基础类型对比
else if (val1 !== val2) {
this.differences.push({
path: path.join('.'),
oldVal: val1,
newVal: val2,
type: 'value_change'
});
}
}
_compareObjects(obj1, obj2, path) {
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
for (const key of allKeys) {
const newPath = [...path, key];
if (!(key in obj1)) {
this.differences.push({
path: newPath.join('.'),
oldVal: undefined,
newVal: obj2[key],
type: 'added'
});
} else if (!(key in obj2)) {
this.differences.push({
path: newPath.join('.'),
oldVal: obj1[key],
newVal: undefined,
type: 'removed'
});
} else {
this._compareRecursive(obj1[key], obj2[key], newPath);
}
}
}
_compareArrays(arr1, arr2, path) {
const maxLen = Math.max(arr1.length, arr2.length);
for (let i = 0; i < maxLen; i++) {
if (i >= arr1.length) {
this.differences.push({
path: ${path.join('.')}.[${i}],
oldVal: undefined,
newVal: arr2[i],
type: 'array_added'
});
} else if (i >= arr2.length) {
this.differences.push({
path: ${path.join('.')}.[${i}],
oldVal: arr1[i],
newVal: undefined,
type: 'array_removed'
});
} else {
this._compareRecursive(arr1[i], arr2[i], [...path, [${i}]]);
}
}
}
}
实际使用效果
上面这个类用起来很简单,传入两个对象就能得到详细的差异信息:
const comparator = new DataComparator();
const oldData = {
name: "张三",
age: 25,
hobbies: ["篮球", "游泳"],
address: {
city: "北京",
district: "朝阳区"
}
};
const newData = {
name: "李四",
age: 26,
hobbies: ["篮球", "游泳", "阅读"],
address: {
city: "上海",
district: "浦东新区"
},
job: "工程师" // 新增字段
};
const differences = comparator.compare(oldData, newData);
console.log(differences);
// 输出结果:
// [
// { path: "name", oldVal: "张三", newVal: "李四", type: "value_change" },
// { path: "age", oldVal: 25, newVal: 26, type: "value_change" },
// { path: "address.city", oldVal: "北京", newVal: "上海", type: "value_change" },
// { path: "address.district", oldVal: "朝阳区", newVal: "浦东新区", type: "value_change" },
// { path: "hobbies.[2]", oldVal: undefined, newVal: "阅读", type: "array_added" },
// { path: "job", oldVal: undefined, newVal: "工程师", type: "added" }
// ]
表格形式的可视化对比
光有差异数据还不够直观,通常还需要在界面上展示出来。我配合一个React组件来展示:
import React from 'react';
const DiffTable = ({ differences }) => {
const getChangeTypeText = (type) => {
switch(type) {
case 'value_change': return '值变更';
case 'added': return '新增字段';
case 'removed': return '删除字段';
case 'array_added': return '数组新增';
case 'array_removed': return '数组删除';
case 'type_change': return '类型变更';
default: return '变更';
}
};
const getChangeClass = (type) => {
switch(type) {
case 'added':
case 'array_added':
return 'bg-green-100';
case 'removed':
case 'array_removed':
return 'bg-red-100';
default:
return 'bg-yellow-100';
}
};
return (
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="border p-2 text-left">路径</th>
<th className="border p-2 text-left">变更类型</th>
<th className="border p-2 text-left">原值</th>
<th className="border p-2 text-left">新值</th>
</tr>
</thead>
<tbody>
{differences.map((diff, index) => (
<tr key={index} className={getChangeClass(diff.type)}>
<td className="border p-2 font-mono">{diff.path}</td>
<td className="border p-2">{getChangeTypeText(diff.type)}</td>
<td className="border p-2">{String(diff.oldVal)}</td>
<td className="border p-2">{String(diff.newVal)}</td>
</tr>
))}
</tbody>
</table>
);
};
性能优化:大数据量的对比策略
这里踩过坑,当初直接拿上面那个类对比一个几百K的对象,页面直接卡死了。后来改用了分批处理的方式:
class OptimizedComparator extends DataComparator {
async compareAsync(obj1, obj2, batchSize = 100) {
this.differences = [];
let currentBatch = 0;
const allPaths = this._getAllPaths(obj1, obj2);
for (let i = 0; i < allPaths.length; i += batchSize) {
const batch = allPaths.slice(i, i + batchSize);
await this._processBatch(obj1, obj2, batch);
// 让出主线程执行权
await new Promise(resolve => setTimeout(resolve, 0));
currentBatch++;
if (window.onProgress) {
window.onProgress(Math.min(100, (currentBatch * batchSize / allPaths.length) * 100));
}
}
return this.differences;
}
_getAllPaths(obj1, obj2, path = [], paths = []) {
if (typeof obj1 === 'object' && obj1 !== null && typeof obj2 === 'object' && obj2 !== null) {
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
for (const key of allKeys) {
const newPath = [...path, key];
paths.push(newPath);
const val1 = obj1[key];
const val2 = obj2[key];
if (typeof val1 === 'object' && val1 !== null && typeof val2 === 'object' && val2 !== null) {
this._getAllPaths(val1, val2, newPath, paths);
}
}
}
return paths;
}
_processBatch(obj1, obj2, paths) {
paths.forEach(path => {
let val1 = obj1;
let val2 = obj2;
for (const key of path) {
val1 = val1?.[key];
val2 = val2?.[key];
}
this._compareRecursive(val1, val2, path);
});
}
}
JSON字符串对比的特殊处理
有时候遇到JSON字符串需要对比,不能直接parse,因为可能包含特殊格式的数据。我这里加了一个JSON安全对比方法:
compareJsonStrings(jsonStr1, jsonStr2) {
try {
const obj1 = JSON.parse(jsonStr1);
const obj2 = JSON.parse(jsonStr2);
return this.compare(obj1, obj2);
} catch (e) {
// JSON解析失败时,按字符串对比
if (jsonStr1 !== jsonStr2) {
return [{
path: 'root',
oldVal: jsonStr1,
newVal: jsonStr2,
type: 'string_change'
}];
}
return [];
}
}
踩坑提醒:这几点一定要注意
- Date对象对比:JavaScript的Date对象比较容易踩坑,需要特殊处理。两个相同时间的Date对象用===比较是false的。
- 循环引用:如果对象内部有循环引用,递归对比会导致栈溢出。需要增加访问过的对象缓存。
- undefined和null的区别:有些业务场景需要区分undefined和null,这个在差异对比时要特别注意。
- 数组排序影响:[1,2,3]和[3,2,1]是不同的数组,但在某些业务场景下可能不算差异,这个需求要提前确认清楚。
还有一个坑就是正则表达式、函数等特殊类型的处理,这些通常不在普通的数据对比范围内,但如果遇到需要特殊处理。
扩展用法:结合API请求验证
我经常把数据对比用在API测试上,特别是验证接口返回的数据是否符合预期:
// 测试API返回数据
async function testApiChanges(apiUrl, expectedData) {
const response = await fetch(apiUrl);
const actualData = await response.json();
const comparator = new DataComparator();
const differences = comparator.compare(expectedData, actualData);
if (differences.length > 0) {
console.warn('API响应与预期不符:', differences);
// 这里可以触发告警或者记录日志
}
return differences;
}
// 使用示例
testApiChanges('https://jztheme.com/api/user/123', expectedUserData)
.then(diffs => {
if (diffs.length === 0) {
console.log('API响应正常');
}
});
这个对比工具还能怎么玩
除了基本的差异对比,这个工具还能做一些高级玩法。比如生成数据迁移脚本、做数据版本管理、甚至是实现类似Git的文件对比功能。数据对比的核心价值在于它能清晰地告诉我们哪些东西变了,怎么变的,这对于复杂系统的数据管理和调试非常有用。
另外还可以集成到各种业务场景里,比如表单提交前的数据变化提示、配置管理系统的历史对比、数据库记录的变更追踪等等。
以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,后续会继续分享这类博客。
本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。

暂无评论