From c516d392259c18b88f1d2edd6e89f10b8b48186c Mon Sep 17 00:00:00 2001 From: Kirk Lin Date: Sat, 10 Jun 2023 09:21:20 +0800 Subject: [PATCH] fix(deepMerge): the default merge strategy is to replace the array (#2843) --- src/utils/__test__/index.test.ts | 118 +++++++++++++++++++++++++++++++ src/utils/index.ts | 57 ++++++++++----- 2 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 src/utils/__test__/index.test.ts diff --git a/src/utils/__test__/index.test.ts b/src/utils/__test__/index.test.ts new file mode 100644 index 000000000..83be4facb --- /dev/null +++ b/src/utils/__test__/index.test.ts @@ -0,0 +1,118 @@ +// 暂时未安装依赖,无法测试 +// @ts-ignore +import { describe, expect, test } from 'vitest'; +import { deepMerge } from '@/utils'; + +describe('deepMerge function', () => { + test('should merge two objects recursively', () => { + const source = { + a: { b: { c: 1 }, d: [1, 2] }, + e: [1, 2], + foo: { bar: 3 }, + array: [ + { + does: 'work', + too: [1, 2, 3], + }, + ], + }; + const target = { + a: { b: { d: [3] } }, + e: [3], + foo: { baz: 4 }, + qu: 5, + array: [ + { + does: 'work', + too: [4, 5, 6], + }, + { + really: 'yes', + }, + ], + }; + const expected = { + a: { b: { c: 1, d: [3] }, d: [1, 2] }, + e: [3], + foo: { + bar: 3, + baz: 4, + }, + array: [ + { + does: 'work', + too: [4, 5, 6], + }, + { + really: 'yes', + }, + ], + qu: 5, + }; + expect(deepMerge(source, target)).toEqual(expected); + }); + + test('should replace arrays by default', () => { + const source = { + a: { b: { d: [1, 2] } }, + e: [1, 2], + }; + const target = { + a: { b: { d: [3] } }, + e: [3], + }; + const expected = { + a: { b: { d: [3] } }, + e: [3], + }; + expect(deepMerge(source, target)).toEqual(expected); + }); + + test("should union arrays using mergeArrays = 'union'", () => { + const source = { + a: { b: { d: [1, 2] } }, + e: [1, 2], + }; + const target = { + a: { b: { d: [2, 3] } }, + e: [3], + }; + const expected = { + a: { b: { d: [1, 2, 3] } }, + e: [1, 2, 3], + }; + expect(deepMerge(source, target, 'union')).toEqual(expected); + }); + + test("should intersect arrays using mergeArrays = 'intersection'", () => { + const source = { + a: { b: { d: [1, 2] } }, + e: [1, 2], + }; + const target = { + a: { b: { d: [2, 3] } }, + e: [3], + }; + const expected = { + a: { b: { d: [2] } }, + e: [], + }; + expect(deepMerge(source, target, 'intersection')).toEqual(expected); + }); + + test("should concatenate arrays using mergeArrays = 'concat'", () => { + const source = { + a: { b: { d: [1, 2] } }, + e: [1, 2], + }; + const target = { + a: { b: { d: [2, 3] } }, + e: [3], + }; + const expected = { + a: { b: { d: [1, 2, 2, 3] } }, + e: [1, 2, 3], + }; + expect(deepMerge(source, target, 'concat')).toEqual(expected); + }); +}); diff --git a/src/utils/index.ts b/src/utils/index.ts index 301226b1e..219107829 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,7 +1,7 @@ import type { App, Component } from 'vue'; import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router'; -import { cloneDeep, mergeWith, uniq } from 'lodash-es'; +import { intersectionWith, isEqual, mergeWith, unionWith } from 'lodash-es'; import { unref } from 'vue'; import { isArray, isObject } from '/@/utils/is'; @@ -34,25 +34,50 @@ export function setObjToUrlParams(baseUrl: string, obj: any): string { } /** - - 递归合并两个对象。 - Recursively merge two objects. - @param target 目标对象,合并后结果存放于此。The target object to merge into. - @param source 要合并的源对象。The source object to merge from. - @returns 合并后的对象。The merged object. + * Recursively merge two objects. + * 递归合并两个对象。 + * + * @param source The source object to merge from. 要合并的源对象。 + * @param target The target object to merge into. 目标对象,合并后结果存放于此。 + * @param mergeArrays How to merge arrays. Default is "replace". + * 如何合并数组。默认为replace。 + * - "union": Union the arrays. 对数组执行并集操作。 + * - "intersection": Intersect the arrays. 对数组执行交集操作。 + * - "concat": Concatenate the arrays. 连接数组。 + * - "replace": Replace the source array with the target array. 用目标数组替换源数组。 + * @returns The merged object. 合并后的对象。 */ export function deepMerge( - target: T, - source: U, + source: T, + target: U, + mergeArrays: 'union' | 'intersection' | 'concat' | 'replace' = 'replace', ): T & U { - return mergeWith(cloneDeep(target), source, (objValue, srcValue) => { - if (isObject(objValue) && isObject(srcValue)) { - return mergeWith(cloneDeep(objValue), srcValue, (prevValue, nextValue) => { - // 如果是数组,合并数组(去重) If it is an array, merge the array (remove duplicates) - return isArray(prevValue) ? uniq(prevValue, nextValue) : undefined; - }); + if (!target) { + return source as T & U; + } + if (!source) { + return target as T & U; + } + if (isArray(target) && isArray(source)) { + switch (mergeArrays) { + case 'union': + return unionWith(target, source, isEqual) as T & U; + case 'intersection': + return intersectionWith(target, source, isEqual) as T & U; + case 'concat': + return target.concat(source) as T & U; + case 'replace': + return source as T & U; + default: + throw new Error(`Unknown merge array strategy: ${mergeArrays}`); } - }); + } + if (isObject(target) && isObject(source)) { + return mergeWith({}, target, source, (targetValue, sourceValue) => { + return deepMerge(targetValue, sourceValue, mergeArrays); + }) as T & U; + } + return source as T & U; } export function openWindow(