「前端基础」从零实现JSON-Diff并深度检测

2,637 阅读5分钟

前言

在一些场景中会出现需要对两个JSON数据进行比较,并标注对应的增加、删除、修改处。本文基于TypeScript实现JSON-Diff方法,用于处理通用场景。

实现功能

由于需要将不同变化情况的数据进行不同的标注表示,所以先初步定义以下功能:

  • 比较两个 JSON 数据,并返回发生变化的 key
  • 深度比较内部嵌套,并返回 json[key][key]的形式。
  • 深度比较「字符串/数组」类型数据,并返回内部修改的数据索引。

1. 定义接口

首先定义最内层的数据结构,也就是diff后的结果。

interface DiffDetail {
    'old': any,
    'new': any
}

对比后分别返回oldnew的结果。由于当数据内容为String时,需要深度遍历内部变化,因此在外层设置一个DiffData用于包裹DiffDetail

interface DiffData {
    // 是否需要深度遍历字符串等
    deepType?: String,
    // diff的两个数据类型
    type?: String,
    // diff结果,字符串为DiffData,否则为DiffDetail
    detail?: DiffDetail | DiffData
}

当遍历到字符串时,再包裹一层DiffData用于展示字符串内部的变化情况。

最后定义最终返回的结果。

interface Result {
    readonly type: String,
    data: DiffData,
    success: Boolean,
    errorMsg?: String
}

其中data存放diff后的结果。

2. 使用方法

定义两个简单的json

const json1 = {
    "a": 1,
    "b": 2,
    "e": 0
}
const json2 = {
    "a": 1,
    "b": 3,
    "c": 4
}

调用diff方法后返回数据为

Result {
    "type": "Object",
    "data": {
        "b": {
            "type": "modify",
            "detail": {
                "old": 2, 
                "new": 3
            }
        },
        "e": {
            "type": "delete",
            "detail": {
                "old": 0, 
                "new": null
            }
        },
        "c": {
            "type": "add", 
            "detail": {
                "old": null, 
                "new": 4
            }
        }
    },
    "success": true
}

只需要判断对应Result中是否存在对应的key,就知道是否发生变化,其中返回的type也可以作为DOM使用的类名。

3. 基本思路

针对两个json数据的比较,由于存在前者的数据后者没有的情况,或后者新增某项值。此时想到的是合并两个json再进行比较,但是合并的过程又需要递归,效率较低。
因此可以遍历其中一个json,如遍历json1当其中的keyjson2中找不到时,代表数据被删除。当key能找到,代表数据既不增加也不删除,此时在json2中删除对应的key。遍历完成后json2中剩余的即新增的key

4. 定义Diff方法

4.1 数据校验

首先在最外层做数据的校验,校验后进行遍历比较。

/**
 * Diff
 * @param payload1 {array | object}
 * @param payload2 {array | object}
 * @returns
 * Object {
 *   [key | index]: {
 *     type: modify | add | delete,
 *     detail: {
 *       old: any,
 *       new: any
 *     }
 *   }
 * }
 */
function Diff(payload1: Array<any> | Object, payload2: Array<any> | Object): Object {

    const result: Result = {
        type: getInstance(payload2),
        data: {},
        success: true
    };

    // 判断基本类型不同
    if (typeof payload1 !== typeof payload2) {
        result.success = false;
        result.errorMsg = '数据基本类型不同';
        return result;
    }
    // 判断引用类型不同
    if (!equalType(payload1, payload2)) {
        result.success = false;
        result.errorMsg = '数据类型不同';
        return result;
    }

    diffObject(result.data, payload1, payload2);

    return result;
}

其中定义了两个简单的工具函数getInstanceequalType

/**
 * 判断属于相同的类型
 * @param obj1 {Object}
 * @param obj2 {Object}
 * @returns Boolean
 */
function equalType(obj1: Object, obj2: Object): Boolean {
    if (typeof obj1 !== 'object' && typeof obj1 !== 'function') return false;
    return getInstance(obj1) === getInstance(obj2);
}

/**
 * 获取引用数据具体类型字符串
 * @param obj {any}
 * @return String
 */
function getInstance(obj: any): String {
    if (typeof obj !== 'object' && typeof obj !== 'function') return typeof obj;
    const _toString: Function = Object.prototype.toString;
    return _toString.call(obj).slice(8, -1);
}

校验数据后开始diffObject函数。

4.2 diffObject

定好基本思路后,diffObject方法只需要实现对应的循环和递归即可。

遍历过程中共有以下几种情况分别进行处理:

  • 「删除」json2中不存在对应key
  • 「修改并递归」json1json2均不为基本数据类型,且数据结构相同
  • 「修改并递归」json1json2均为String类型
  • 「修改」json1json2数据结构不同或值不同
  • 「不变」json1json2相同
  • 「新增」json2中剩余的key

最终完成整理代码逻辑如下:

/**
 * diff - Object类型数据
 * @param result {object} diff结果对象
 * @param payload1 {object}
 * @param payload2 {object}
 */
function diffObject(result: Object, payload1: Object, payload2: Object): void {
    // 深拷贝payload2,避免影响原数据
    const _obj2: Object = deepClone(payload2);
    for (let key in payload1) {
        // json2中不存在时代表删除属性
        if (!_obj2[key]) {
            result[key] = {
                type: 'delete',
                detail: {
                    old: payload1[key],
                    new: null
                }
            };
            continue;
        }

        // 数据结构相同且不为基本数据类型时
        if (equalType(payload1[key], payload2[key])) {
            result[key] = result[key] ? result[key] : {};
            // 递归子属性
            diffObject(result[key], payload1[key], payload2[key]);
            // 删除对应json2上的key
            delete _obj2[key];
            continue;
        }

        // 数据类型为字符串且长度大于1时深度diff字符串
        if (payload1[key] !== payload2[key] && typeof payload1[key] === 'string' && typeof payload2[key] === 'string' && (payload1[key].length > 1 || payload2[key].length > 1)) {
            result[key] = result[key] ? result[key] : {
                deepType: 'String',
                type: 'modify',
                detail: {}
            };
            // 将字符串转为数组并递归
            diffObject(result[key].detail, payload1[key].split(''), payload2[key].split(''));
            // 删除对应json2上的key
            delete _obj2[key];
            continue;
        }

        // 基本类型直接比较是否不同
        if (payload2[key] && (payload1[key] !== payload2[key])) {
            result[key] = {
                type: 'modify',
                detail: {
                    old: payload1[key],
                    new: payload2[key]
                }
            };
            // 删除对应json2上的key
            delete _obj2[key];
            continue;
        }
        // 相同情况,删除json2中的key
        delete _obj2[key];
    }

    // json2剩余的均为新增
    for (let key in _obj2) {
        result[key] = {
            type: 'add',
            detail: {
                old: null,
                new: payload2[key]
            }
        };
    }
}

至此,整个Diff函数完成,测试结果与预期一致。

5. 测试

输入两个json

// json1
{
    "a": "test1",
    "b": "test2",
    "e": 0,
    "x": {
        "y": 12
    }
}
// json2
{
    "a": 1,
    "b": "test3",
    "c": 4,
    "x": {
        "y": 13
    }
}

调用Diff(json1, json2)后得到的结果为

Result {
    "type": "Object",
    "data": {
        "a": {
            "type": "modify",
            "detail": {
                "old": "test1",
                "new": 1
            }
        },
        "b": {
            "deepTpye": "String",
            "type": "modify",
            "detail": {
                "4": {
                    "type": "modify",
                    "detail": {
                        "old": "2",
                        "new": "3"
                    }
                }
            }
        },
        "e": {
            "type": "delete",
            "detail": {
                "old": 0,
                "new": null
            }
        },
        "x": {
            "y": {
                "type": "modify",
                "detail": {
                    "old": 12,
                    "new": 13
                }
            }
        },
        "c": {
            "type": "add",
            "detail": {
                "old": null,
                "new": 4
            }
        }
    },
    "success": true
}

大致覆盖了一些基本情况,有其他测试用例的小伙伴可以帮忙找出bug,文章中有哪些错误的内容也欢迎大家及时指正。