数据对比场景下的前端实现方案与常见陷阱

UI国娟 交互 阅读 1,055
赞 20 收藏
二维码
手机扫码查看
反馈

先来个简单的数据对比工具类

最近项目里经常需要做数据对比,特别是用户修改配置后的差异展示,之前都是手动写一堆判断逻辑,太麻烦了。今天把我封装的一个数据对比工具类分享出来,亲测有效。

数据对比场景下的前端实现方案与常见陷阱

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(&#039;.&#039;)}.[${i}],
                    oldVal: undefined,
                    newVal: arr2[i],
                    type: 'array_added'
                });
            } else if (i >= arr2.length) {
                this.differences.push({
                    path: ${path.join(&#039;.&#039;)}.[${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立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论