源码地址。
需要先安装依赖:
npm install deep-equal diff-match-patch
npm i @types/diff-match-patch
npm install --save-dev @types/deep-equal
实现方式:
import equal from 'deep-equal';
import DiffMatchPatch from 'diff-match-patch';
const diffMatchPatchInstance = new DiffMatchPatch();
type JsonMLPath = (string | number)[];
type Ops = any[];
interface Json1 {
replaceOp: (path: JsonMLPath, input: any, output: any) => any;
removeOp: (path: JsonMLPath, input: any) => any;
insertOp: (path: JsonMLPath, output: any) => any;
editOp: (path: JsonMLPath, type: any, unicodeOp: any) => any;
type: {
compose: (a: any, b: any) => any;
};
}
function replaceOp(path: JsonMLPath, isObject: boolean, input: any, output: any, json1?: Json1): Ops {
let op: any;
if (json1) {
op = json1.replaceOp(path, input, output);
} else {
op = { p: path };
op[isObject ? 'od' : 'ld'] = input;
op[isObject ? 'oi' : 'li'] = output;
}
return [op];
}
function patchesToOps(
path: JsonMLPath,
oldValue: string,
newValue: string,
diffMatchPatch: typeof DiffMatchPatch,
json1?: Json1,
textUnicode?: any
): Ops {
const ops: Ops = [];
const patches = diffMatchPatchInstance.patch_make(oldValue, newValue);
patches.forEach((patch: any) => {
let offset = patch.start1;
// @ts-ignore
patch.diffs.forEach(([type, value]) => {
// @ts-ignore
switch (type) {
case diffMatchPatch.DIFF_DELETE:
if (textUnicode) {
const unicodeOp = textUnicode.remove(offset, value);
ops.push(json1!.editOp(path, textUnicode.type, unicodeOp));
} else {
ops.push({ sd: value, p: [...path, offset] });
}
break;
// @ts-ignore
case diffMatchPatch.DIFF_INSERT:
if (textUnicode) {
const unicodeOp = textUnicode.insert(offset, value);
ops.push(json1!.editOp(path, textUnicode.type, unicodeOp));
} else {
ops.push({ si: value, p: [...path, offset] });
}
case diffMatchPatch.DIFF_EQUAL:
offset += value.length;
break;
default:
throw new Error(`Unsupported operation type: ${type}`);
}
});
});
return ops;
}
function optimize(ops: Ops, options?: { json1?: Json1 }): Ops {
if (options && options.json1) {
const compose = options.json1.type.compose;
return ops.reduce(compose, null);
}
for (let i = 0, l = ops.length - 1; i < l; ++i) {
const a = ops[i], b = ops[i + 1];
if (!equal(a.p.slice(0, -1), b.p.slice(0, -1))) {
continue;
}
if (a.p[a.p.length - 1] + 1 !== b.p[b.p.length - 1]) {
continue;
}
if (!a.li || !b.ld) {
continue;
}
if (!equal(a.li, b.ld)) {
continue;
}
delete a.li;
delete b.ld;
}
return ops.filter(op => Object.keys(op).length > 1);
}
function diff(
input: any,
output: any,
path: JsonMLPath = [],
options?: { diffMatchPatch?: typeof DiffMatchPatch, json1?: Json1, textUnicode?: any }
): Ops {
const diffMatchPatch = options && options.diffMatchPatch;
const json1 = options && options.json1;
const textUnicode = options && options.textUnicode;
const isObject = typeof path[path.length - 1] === 'string' || path.length === 0;
if (equal(input, output) && Array.isArray(input) === Array.isArray(output)) {
return [];
}
if (typeof output === 'undefined') {
let op: any;
if (json1) {
op = json1.removeOp(path, input);
} else {
op = { p: path };
op[isObject ? 'od' : 'ld'] = input;
}
return [op];
}
if (typeof input === 'undefined') {
let op: any;
if (json1) {
op = json1.insertOp(path, output);
} else {
op = { p: path };
op[isObject ? 'oi' : 'li'] = output;
}
return [op];
}
if (input === null || output === null) {
const ops: Ops = [];
if (json1) {
ops.push(json1.removeOp(path, input));
ops.push(json1.insertOp(path, output));
} else {
const op_delete: any = { p: path };
op_delete[isObject ? 'od' : 'ld'] = input;
const op_insert: any = { p: path };
op_insert[isObject ? 'oi' : 'li'] = output;
ops.push(op_delete, op_insert);
}
return ops;
}
if (diffMatchPatch && typeof input === 'string' && typeof output === 'string') {
return patchesToOps(path, input, output, diffMatchPatch, json1, textUnicode);
}
const primitiveTypes = ['string', 'number', 'boolean'];
if (primitiveTypes.includes(typeof output) || primitiveTypes.includes(typeof input)) {
return replaceOp(path, isObject, input, output, json1);
}
if (Array.isArray(output) && Array.isArray(input)) {
const ops: Ops = [];
const inputLen = input.length, outputLen = output.length;
const minLen = Math.min(inputLen, outputLen);
for (let i = 0; i < minLen; ++i) {
const newOps = diff(input[i], output[i], [...path, i], options);
ops.push(...newOps);
}
if (outputLen > inputLen) {
for (let i = minLen; i < outputLen; i++) {
const newOps = diff(undefined, output[i], [...path, i], options);
ops.push(...newOps);
}
} else if (outputLen < inputLen) {
for (let i = minLen; i < inputLen; i++) {
const newOps = diff(input[i], undefined, [...path, minLen], options);
ops.push(...newOps);
}
}
return ops;
} else if (Array.isArray(output) || Array.isArray(input)) {
return replaceOp(path, isObject, input, output, json1);
}
let ops: Ops = [];
const keys = new Set([...Object.keys(input), ...Object.keys(output)]);
keys.forEach(key => {
const newOps = diff(input[key], output[key], [...path, key], options);
ops = ops.concat(newOps);
});
return ops;
}
function optimizedDiff(
input: any,
output: any,
diffMatchPatch?: typeof DiffMatchPatch,
json1?: Json1,
textUnicode?: any
): Ops {
const options = { diffMatchPatch, json1, textUnicode };
return optimize(diff(input, output, [], options), options);
}
export { optimizedDiff };
调用方式:
const a = [
{
"recordId": "rec00001"
},
{
"recordId": "rec00002"
},
{
"recordId": "rec00003"
},
{
"recordId": "rec00008"
},
{
"recordId": "rec00004"
}
];
const b = [
{
"recordId": "rec00001"
},
{
"recordId": "rec00005"
},
{
"recordId": "rec00002"
},
{
"recordId": "rec00003"
},
{
"recordId": "rec00007"
},
{
"recordId": "rec00004"
}
];
var diff = optimizedDiff(
a,
b
);