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 formattedKey = needsEscaping ? `["${key}"]` : `.${key}`;
|
||||
|
||||
return parent === ROOT_PATH ? `${ROOT_PATH}${formattedKey}` : `${parent}${formattedKey}`;
|
||||
};
|
||||
|
||||
@@ -30,104 +29,48 @@ const primitiveEqual = (a: unknown, b: unknown): boolean => {
|
||||
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,
|
||||
right: MaybeMissing,
|
||||
key: string,
|
||||
path: string,
|
||||
diffPaths: string[],
|
||||
): 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) {
|
||||
): DiffNode | null => {
|
||||
if (left === null || right === null) {
|
||||
if (left === null && right === null) {
|
||||
return {
|
||||
@@ -140,8 +83,9 @@ const diffNode = (
|
||||
hasDiffInChildren: false,
|
||||
};
|
||||
}
|
||||
} else if (typeof left === typeof right) {
|
||||
if (primitiveEqual(left, right)) {
|
||||
return null;
|
||||
}
|
||||
if (typeof left === typeof right && primitiveEqual(left, right)) {
|
||||
return {
|
||||
key,
|
||||
type: 'unchanged',
|
||||
@@ -152,27 +96,172 @@ const diffNode = (
|
||||
hasDiffInChildren: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
diffPaths.push(path);
|
||||
return {
|
||||
key,
|
||||
type: 'modified',
|
||||
oldValue: left,
|
||||
newValue: right,
|
||||
path,
|
||||
isLeaf: !leftIsContainer && !rightIsContainer,
|
||||
hasDiffInChildren: false,
|
||||
};
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 比较两个 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 => {
|
||||
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 {
|
||||
root,
|
||||
diffPaths,
|
||||
|
||||
Reference in New Issue
Block a user