refactor(utils): 将 diffNode 实现从递归改为迭代遍历
This commit is contained in:
+201
-112
@@ -19,7 +19,6 @@ const buildPath = (parent: string, key: string, isArrayChild: boolean): string =
|
|||||||
}
|
}
|
||||||
const needsEscaping = key.includes('.') || key.includes('[') || key.includes(' ');
|
const needsEscaping = key.includes('.') || key.includes('[') || key.includes(' ');
|
||||||
const formattedKey = needsEscaping ? `["${key}"]` : `.${key}`;
|
const formattedKey = needsEscaping ? `["${key}"]` : `.${key}`;
|
||||||
|
|
||||||
return parent === ROOT_PATH ? `${ROOT_PATH}${formattedKey}` : `${parent}${formattedKey}`;
|
return parent === ROOT_PATH ? `${ROOT_PATH}${formattedKey}` : `${parent}${formattedKey}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,104 +29,48 @@ const primitiveEqual = (a: unknown, b: unknown): boolean => {
|
|||||||
return a === b;
|
return a === b;
|
||||||
};
|
};
|
||||||
|
|
||||||
const diffNode = (
|
// ---------- 迭代遍历框架 ----------
|
||||||
|
|
||||||
|
interface StackFrame {
|
||||||
|
left: MaybeMissing;
|
||||||
|
right: MaybeMissing;
|
||||||
|
key: string;
|
||||||
|
path: string;
|
||||||
|
/** 子节点数量(组装时从 resultStack 弹出) */
|
||||||
|
childCount: number;
|
||||||
|
/** 是否已展开 */
|
||||||
|
assembled: boolean;
|
||||||
|
/** 分类结果缓存 */
|
||||||
|
leftObj: boolean;
|
||||||
|
rightObj: boolean;
|
||||||
|
leftArr: boolean;
|
||||||
|
rightArr: boolean;
|
||||||
|
leftIsContainer: boolean;
|
||||||
|
rightIsContainer: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createLeafNode = (
|
||||||
|
key: string,
|
||||||
|
path: string,
|
||||||
|
type: DiffNode['type'],
|
||||||
|
oldValue: MaybeMissing,
|
||||||
|
newValue: MaybeMissing,
|
||||||
|
): DiffNode => ({
|
||||||
|
key,
|
||||||
|
type,
|
||||||
|
oldValue: oldValue === SENTINEL ? undefined : oldValue,
|
||||||
|
newValue: newValue === SENTINEL ? undefined : newValue,
|
||||||
|
path,
|
||||||
|
isLeaf: true,
|
||||||
|
hasDiffInChildren: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const classifyPrimitive = (
|
||||||
left: MaybeMissing,
|
left: MaybeMissing,
|
||||||
right: MaybeMissing,
|
right: MaybeMissing,
|
||||||
key: string,
|
key: string,
|
||||||
path: string,
|
path: string,
|
||||||
diffPaths: string[],
|
): DiffNode | null => {
|
||||||
): DiffNode => {
|
|
||||||
if (left === SENTINEL && right !== SENTINEL) {
|
|
||||||
diffPaths.push(path);
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
type: 'added',
|
|
||||||
oldValue: undefined,
|
|
||||||
newValue: right,
|
|
||||||
path,
|
|
||||||
isLeaf: !isObject(right) && !isArray(right),
|
|
||||||
hasDiffInChildren: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (right === SENTINEL && left !== SENTINEL) {
|
|
||||||
diffPaths.push(path);
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
type: 'removed',
|
|
||||||
oldValue: left,
|
|
||||||
newValue: undefined,
|
|
||||||
path,
|
|
||||||
isLeaf: !isObject(left) && !isArray(left),
|
|
||||||
hasDiffInChildren: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const leftObj = isObject(left);
|
|
||||||
const rightObj = isObject(right);
|
|
||||||
const leftArr = isArray(left);
|
|
||||||
const rightArr = isArray(right);
|
|
||||||
|
|
||||||
if (leftObj && rightObj) {
|
|
||||||
const keySet = new Set<string>();
|
|
||||||
const leftKeys = Object.keys(left);
|
|
||||||
const rightKeys = Object.keys(right);
|
|
||||||
|
|
||||||
for (let i = 0; i < leftKeys.length; i++) keySet.add(leftKeys[i]);
|
|
||||||
for (let i = 0; i < rightKeys.length; i++) keySet.add(rightKeys[i]);
|
|
||||||
|
|
||||||
const children: DiffNode[] = [];
|
|
||||||
keySet.forEach((k) => {
|
|
||||||
const childPath = buildPath(path, k, false);
|
|
||||||
const l: MaybeMissing = k in left ? left[k] : SENTINEL;
|
|
||||||
const r: MaybeMissing = k in right ? right[k] : SENTINEL;
|
|
||||||
children.push(diffNode(l, r, k, childPath, diffPaths));
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasDiffInChildren = children.some((c) => c.type !== 'unchanged' || c.hasDiffInChildren);
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
type: hasDiffInChildren ? 'modified' : 'unchanged',
|
|
||||||
oldValue: left,
|
|
||||||
newValue: right,
|
|
||||||
children,
|
|
||||||
path,
|
|
||||||
isLeaf: false,
|
|
||||||
hasDiffInChildren,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leftArr && rightArr) {
|
|
||||||
const len = Math.max(left.length, right.length);
|
|
||||||
const children: DiffNode[] = new Array(len);
|
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const k = String(i);
|
|
||||||
const childPath = buildPath(path, k, true);
|
|
||||||
const l: MaybeMissing = i < left.length ? left[i] : SENTINEL;
|
|
||||||
const r: MaybeMissing = i < right.length ? right[i] : SENTINEL;
|
|
||||||
children[i] = diffNode(l, r, k, childPath, diffPaths);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasDiffInChildren = children.some((c) => c.type !== 'unchanged' || c.hasDiffInChildren);
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
type: hasDiffInChildren ? 'modified' : 'unchanged',
|
|
||||||
oldValue: left,
|
|
||||||
newValue: right,
|
|
||||||
children,
|
|
||||||
path,
|
|
||||||
isLeaf: false,
|
|
||||||
hasDiffInChildren,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const leftIsContainer = leftObj || leftArr;
|
|
||||||
const rightIsContainer = rightObj || rightArr;
|
|
||||||
|
|
||||||
if (!leftIsContainer && !rightIsContainer) {
|
|
||||||
if (left === null || right === null) {
|
if (left === null || right === null) {
|
||||||
if (left === null && right === null) {
|
if (left === null && right === null) {
|
||||||
return {
|
return {
|
||||||
@@ -140,8 +83,9 @@ const diffNode = (
|
|||||||
hasDiffInChildren: false,
|
hasDiffInChildren: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (typeof left === typeof right) {
|
return null;
|
||||||
if (primitiveEqual(left, right)) {
|
}
|
||||||
|
if (typeof left === typeof right && primitiveEqual(left, right)) {
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
type: 'unchanged',
|
type: 'unchanged',
|
||||||
@@ -152,27 +96,172 @@ const diffNode = (
|
|||||||
hasDiffInChildren: false,
|
hasDiffInChildren: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
diffPaths.push(path);
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
type: 'modified',
|
|
||||||
oldValue: left,
|
|
||||||
newValue: right,
|
|
||||||
path,
|
|
||||||
isLeaf: !leftIsContainer && !rightIsContainer,
|
|
||||||
hasDiffInChildren: false,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 比较两个 JSON 值的差异
|
* 合并对象 key:左全部 + 右独有的(无需 Set,O(K) in 查找)
|
||||||
|
*/
|
||||||
|
const mergedKeys = (left: Record<string, unknown>, right: Record<string, unknown>): string[] => {
|
||||||
|
const result: string[] = Object.keys(left);
|
||||||
|
for (const k of Object.keys(right)) {
|
||||||
|
if (!(k in left)) result.push(k);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较两个 JSON 值的差异(迭代版,显式栈替代递归)
|
||||||
*/
|
*/
|
||||||
export const diffJson = (left: unknown, right: unknown): DiffResult => {
|
export const diffJson = (left: unknown, right: unknown): DiffResult => {
|
||||||
const diffPaths: string[] = [];
|
const diffPaths: string[] = [];
|
||||||
const root = diffNode(left, right, '', ROOT_PATH, diffPaths);
|
|
||||||
|
const rootFrame: StackFrame = {
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
key: '',
|
||||||
|
path: ROOT_PATH,
|
||||||
|
childCount: 0,
|
||||||
|
assembled: false,
|
||||||
|
leftObj: isObject(left),
|
||||||
|
rightObj: isObject(right),
|
||||||
|
leftArr: isArray(left),
|
||||||
|
rightArr: isArray(right),
|
||||||
|
leftIsContainer: isObject(left) || isArray(left),
|
||||||
|
rightIsContainer: isObject(right) || isArray(right),
|
||||||
|
};
|
||||||
|
|
||||||
|
const stack: StackFrame[] = [rootFrame];
|
||||||
|
const resultStack: DiffNode[] = [];
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const frame = stack.pop()!;
|
||||||
|
|
||||||
|
// ---- 组装阶段:从 resultStack 弹出子节点 ----
|
||||||
|
if (frame.assembled) {
|
||||||
|
const { childCount } = frame;
|
||||||
|
const children: DiffNode[] = new Array(childCount);
|
||||||
|
// 子节点按正序压入 resultStack,弹出时逆序取
|
||||||
|
for (let i = childCount - 1; i >= 0; i--) {
|
||||||
|
children[i] = resultStack.pop()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDiffInChildren = children.some((c) => c.type !== 'unchanged' || c.hasDiffInChildren);
|
||||||
|
const type: DiffNode['type'] = hasDiffInChildren ? 'modified' : 'unchanged';
|
||||||
|
|
||||||
|
const node: DiffNode = {
|
||||||
|
key: frame.key,
|
||||||
|
type,
|
||||||
|
oldValue: frame.left === SENTINEL ? undefined : frame.left,
|
||||||
|
newValue: frame.right === SENTINEL ? undefined : frame.right,
|
||||||
|
children,
|
||||||
|
path: frame.path,
|
||||||
|
isLeaf: false,
|
||||||
|
hasDiffInChildren,
|
||||||
|
};
|
||||||
|
resultStack.push(node);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { left: l, right: r } = frame;
|
||||||
|
|
||||||
|
// ---- 一侧缺失 ----
|
||||||
|
if (l === SENTINEL && r !== SENTINEL) {
|
||||||
|
diffPaths.push(frame.path);
|
||||||
|
resultStack.push(createLeafNode(frame.key, frame.path, 'added', undefined, r));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r === SENTINEL && l !== SENTINEL) {
|
||||||
|
diffPaths.push(frame.path);
|
||||||
|
resultStack.push(createLeafNode(frame.key, frame.path, 'removed', l, undefined));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 双侧对象 ----
|
||||||
|
if (frame.leftObj && frame.rightObj) {
|
||||||
|
const keys = mergedKeys(l as Record<string, unknown>, r as Record<string, unknown>);
|
||||||
|
// 压入组装帧
|
||||||
|
stack.push({ ...frame, childCount: keys.length, assembled: true });
|
||||||
|
// 逆序压入子帧,保证正序处理
|
||||||
|
for (let i = keys.length - 1; i >= 0; i--) {
|
||||||
|
const k = keys[i];
|
||||||
|
const childPath = buildPath(frame.path, k, false);
|
||||||
|
const leftVal: MaybeMissing =
|
||||||
|
k in (l as Record<string, unknown>) ? (l as Record<string, unknown>)[k] : SENTINEL;
|
||||||
|
const rightVal: MaybeMissing =
|
||||||
|
k in (r as Record<string, unknown>) ? (r as Record<string, unknown>)[k] : SENTINEL;
|
||||||
|
stack.push({
|
||||||
|
left: leftVal,
|
||||||
|
right: rightVal,
|
||||||
|
key: k,
|
||||||
|
path: childPath,
|
||||||
|
childCount: 0,
|
||||||
|
assembled: false,
|
||||||
|
leftObj: isObject(leftVal),
|
||||||
|
rightObj: isObject(rightVal),
|
||||||
|
leftArr: isArray(leftVal),
|
||||||
|
rightArr: isArray(rightVal),
|
||||||
|
leftIsContainer: isObject(leftVal) || isArray(leftVal),
|
||||||
|
rightIsContainer: isObject(rightVal) || isArray(rightVal),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 双侧数组 ----
|
||||||
|
if (frame.leftArr && frame.rightArr) {
|
||||||
|
const leftArr = l as unknown[];
|
||||||
|
const rightArr = r as unknown[];
|
||||||
|
const len = Math.max(leftArr.length, rightArr.length);
|
||||||
|
|
||||||
|
stack.push({ ...frame, childCount: len, assembled: true });
|
||||||
|
for (let i = len - 1; i >= 0; i--) {
|
||||||
|
const k = String(i);
|
||||||
|
const childPath = buildPath(frame.path, k, true);
|
||||||
|
const leftVal: MaybeMissing = i < leftArr.length ? leftArr[i] : SENTINEL;
|
||||||
|
const rightVal: MaybeMissing = i < rightArr.length ? rightArr[i] : SENTINEL;
|
||||||
|
stack.push({
|
||||||
|
left: leftVal,
|
||||||
|
right: rightVal,
|
||||||
|
key: k,
|
||||||
|
path: childPath,
|
||||||
|
childCount: 0,
|
||||||
|
assembled: false,
|
||||||
|
leftObj: isObject(leftVal),
|
||||||
|
rightObj: isObject(rightVal),
|
||||||
|
leftArr: isArray(leftVal),
|
||||||
|
rightArr: isArray(rightVal),
|
||||||
|
leftIsContainer: isObject(leftVal) || isArray(leftVal),
|
||||||
|
rightIsContainer: isObject(rightVal) || isArray(rightVal),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 原始值比较 ----
|
||||||
|
if (!frame.leftIsContainer && !frame.rightIsContainer) {
|
||||||
|
const primitiveResult = classifyPrimitive(l, r, frame.key, frame.path);
|
||||||
|
if (primitiveResult) {
|
||||||
|
resultStack.push(primitiveResult);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型不同或容器 vs 原始值 → modified
|
||||||
|
diffPaths.push(frame.path);
|
||||||
|
resultStack.push({
|
||||||
|
key: frame.key,
|
||||||
|
type: 'modified',
|
||||||
|
oldValue: l,
|
||||||
|
newValue: r,
|
||||||
|
path: frame.path,
|
||||||
|
isLeaf: !frame.leftIsContainer && !frame.rightIsContainer,
|
||||||
|
hasDiffInChildren: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = resultStack[resultStack.length - 1];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
root,
|
root,
|
||||||
diffPaths,
|
diffPaths,
|
||||||
|
|||||||
Reference in New Issue
Block a user