共计 9435 个字符,预计需要花费 24 分钟才能阅读完成。
在开发 web 应用程序时,性能都是必不可少的话题。同时通用 web 应用程序离不开数据的增删改查,尽管用户大部分操作都是在查问,然而咱们也不能够疏忽更改数据对于零碎的影响。于是集体写了一个业务数据比对库 diff-helper。不便开发者在前端提交数据到服务端时候去除不必要的信息,优化网络传输和服务端性能。
我的项目演进
任何我的项目都不是一触而就的,上面是对于 diff-helper 库的编写思路。心愿能对大家有一些帮忙。
简略对象比对
前端提交 JSON 对象数据时,很多状况下都是对象一层数据比对。在不思考对象中还有简单数据(嵌套对象和数组)的状况下,编写如下代码
// newVal 示意新数据,oldVal 示意老数据
const simpleObjDiff = ({
newVal,
oldVal,
}): Record<string, any> => {
// 以后比对的后果
const diffResult: Record<string, any> = {};
// 曾经查看过的数据项,能够优化遍历性能
const checkedKeys: Set<string> = new Set();
// 遍历最新的对象属性
Object.keys(newVal).forEach((key: string) => {
// 将新数据的 key 记录一下
checkedKeys.add(key);
// 如果以后新的数据不等于老数据,间接把新的比对后果放入
if (newVal[key] !== oldVal[key]) {diffResult[key] = newVal[key];
}
});
// 遍历之前的对象属性
Object.keys(oldVal).forEach((key) => {
// 如果曾经查看过了,不在进行解决
if (checkedKeys.has(key)) {return;}
// 新的数据有,然而老数据没有能够认为数据曾经不存在了
diffResult[key] = null;
});
return diffResult;
};
此时咱们就能够应用该函数进行一系列简略数据操作了。
const result = simpleObjDiff({
newVal: {
a: 1,
b: 1,
},
oldVal: {
a: 2,
c: 2,
},
});
// => 返回后果为
result = {
a: 1,
b: 1,
c: null,
};
增加简单属性比对
以后函数在面对对象外部有简单类型时候就没方法判断了,即便没有更改的状况下,后果也会蕴含新数据属性,然而思考到提交到服务端的表单数据个别不须要增量提交,所以这里试一试 JSON.stringify。
诸如:
JSON.stringify("123");
// '"123"'
JSON.stringify(123);
// '123'
JSON.stringify(new Date());
// '"2022-11-29T15:16:46.325Z"'
JSON.stringify([1, 2, 3]);
// '[1,2,3]'
JSON.stringify({a: 1, b: 2});
// '{"b":2,"a":1}'
JSON.stringify({b: 2, a: 1});
// '{"b":2,"a":1}'
JSON.stringify({b: 2, a: 1}, ["a", "b"]);
// '{"a":1,"b":2}'
JSON.stringify({b: 2, a: 1}, ["a", "b"]) === JSON.stringify({a: 1, b: 2});
// true
比照上述后果,咱们能够看到,JSON.stringify 如果不提供 replacer 可能会对对象类型数据的生成后果产生“误伤”。但从零碎理论运行上来说,对象外部属性不太会呈现排序变动的状况。间接进行以下革新:
const simpleObjDiff = ({
newVal,
oldVal,
}): Record<string, any> => {
// ... 之前的代码
// 遍历最新的对象数据
Object.keys(newVal).forEach((key: string) => {
// 以后曾经解决过的对象 key 记录一下
checkedKeys.add(key);
// 先去查看类型,判断雷同类型后再应用 JSON.stringify 获取字符串后果进行比对
if (typeof newVal[key] !== typeof oldVal[key] ||
JSON.stringify(newVal[key]) !== JSON.stringify(oldVal[key])
) {diffResult[key] = newVal[key];
}
});
// ... 之前的代码
};
这时候尝试一下简单数据类型
const result = simpleObjDiff({
newVal: {
a: 1,
b: 1,
d: [1, 2, 3],
},
oldVal: {
a: 2,
c: 2,
d: [1, 2, 3],
},
});
// => 返回后果为
result = {
a: 1,
b: 1,
c: null,
};
增加自定义对象属性比对
如果只应用 JSON.stringify 话,函数就没有方法灵便的解决各种需要,所以笔者开始追加函数让用户自行适配。
const simpleObjDiff = ({
newVal,
oldVal,
options,
}): Record<string, any> => {
// ... 之前的代码
// 获取用户定义的 diff 函数
const {diffFun} = {...DEFAULT_OPTIONS, ...options};
// 判断以后传入数据是否是函数
const hasDiffFun = typeof diffFun === "function";
// 遍历最新的对象数据
Object.keys(newVal).forEach((key: string) => {
// 以后曾经解决过的对象 key 记录一下
checkedKeys.add(key);
let isChanged = false;
if (hasDiffFun) {
// 把以后属性 key 和对应的新旧值传入从而获取后果
const diffResultByKey = diffFun({
key,
newPropVal: newVal[key],
oldPropVal: oldVal[key],
});
// 返回了后果则写入 diffResult,没有后果认为传入的函数不解决
// 留神是不解决,而不是认为不变动
// 如果没返回就会持续走 JSON.stringify
if (
diffResultByKey !== null &&
diffResultByKey !== undefined
) {diffResult[key] = diffResultByKey;
isChanged = true;
}
}
if (isChanged) {return;}
if (typeof newVal[key] !== typeof oldVal[key] ||
JSON.stringify(newVal[key]) !== JSON.stringify(oldVal[key])
) {diffResult[key] = newVal[key];
}
});
// ... 之前的代码
};
此时咱们尝试传入 diffFun 来看看成果:
const result = simpleObjDiff({
newVal: {a: [12, 3, 4],
b: 11,
},
oldVal: {a: [1, 2, 3],
c: 22,
},
options: {
diffFun: ({
key,
newPropVal,
oldPropVal,
}) => {switch (key) {
// 解决对象中的属性 a
case "a":
// 以后数组新旧数据都有的数据项才会保留下来
return newPropVal.filter((item: any) => oldPropVal.includes(item));
}
// 其余咱们抉择不解决,应用默认的 JSON.stringify
return null;
},
},
});
// => 后果如下所示
result = {a: [3],
b: 11,
c: null,
};
通过 diffFun 函数,开发者岂但能够自定义属性解决,还能够利用 fast-json-stringify 来优化外部属性解决。该库通过 JSON schema 事后告知对象外部的属性类型,在提前晓得数据类型的状况下,针对性解决会让 fast-json-stringify 性能十分高。
import fastJson from "fast-json-stringify";
const stringify = fastJson({
title: "User Schema",
type: "object",
properties: {
firstName: {type: "string",},
lastName: {type: "string",},
age: {
description: "Age in years",
type: "integer",
},
},
});
stringify({
firstName: "Matteo",
lastName: "Collina",
age: 32,
});
// "{\"firstName\":\"Matteo\",\"lastName\":\"Collina\",\"age\":32}"
stringify({
lastName: "Collina",
age: 32,
firstName: "Matteo",
});// "{\"firstName\":\"Matteo\",\"lastName\":\"Collina\",\"age\":32}"
能够看到,利用 fast-json-stringify 同时无需思考对象属性的外部程序。
增加其余解决
这时候开始解决其余问题:
// 增加异样谬误抛出
const invariant = (condition: boolean, errorMsg: string) => {if (condition) {throw new Error(errorMsg);
}
};
// 判断是否是实在的对象
const isRealObject = (val: any): val is Record<string, any> => {return Object.prototype.toString.call(val) === "[object Object]";
};
simpleObjDiff = ({
newVal,
oldVal,
options,
}: SimpleObjDiffParams): Record<string, any> => {
// 增加谬误传参解决
invariant(!isRealObject(newVal), "params newVal must be a Object");
invariant(!isRealObject(oldVal), "params oldVal must be a Object");
// ...
const {diffFun, empty} = {...DEFAULT_OPTIONS, ...options};
// ...
Object.keys(oldVal).forEach((key) => {
// 如果曾经查看过了,间接返回
if (checkedKeys.has(key)) {return;}
// 设定空数据,倡议应用 null 或 空字符串
diffResult[key] = empty;
});
};
简略对象比对函数就根本实现了。有趣味的同学也能够间接浏览 obj-diff 源码。
简略数组比照
接下来就开始解决数组了,数组的比对外围在于数据的主键辨认。代码如下:
const simpleListDiff = ({
newVal,
oldVal,
options,
}: SimpleObjDiffParams) => {const opts = { ...DEFAULT_OPTIONS, ...options};
// 获取以后的主键 key 数值,不传递 key 默认为 'id'
const {key, getChangedItem} = opts;
// 增删改的数据
const addLines = [];
const deletedLines = [];
const modifiedLines = [];
// 增加检测过的数组主键,ListKey 是数字或者字符串类型
const checkedKeys: Set<ListKey> = new Set<ListKey>();
// 开始进行传入数组遍历
newVal.forEach((newLine) => {
// 依据主键去寻找之前的数据,也有可能新数据没有 key,这时候也是找不到的
let oldLine: any = oldVal.find((x) => x[key] === newLine[key]);
// 发现之前没有,走增加数据逻辑
if (!oldLine) {addLines.push(newLine);
} else {
// 更新的数据 id 增加到 checkedKeys 外面去,不便删除
checkedKeys.add(oldLine[key]);
// 传入函数 getChangedItem 来获取后果
const result = getChangedItem!({
newLine,
oldLine,
});
// 没有后果则认为以后数据没有改过, 无需解决
// 留神,和下面不同,这里返回 null 则认为数据没有批改
if (result !== null && result !== undefined) {modifiedLines.push(result);
}
}
});
oldVal.forEach((oldLine) => {
// 之前更新过不必解决
if (checkedKeys.has(oldLine[key])) {return;}
// 剩下的都是删除的数据
deletedLines.push({[key]: oldLine[key],
});
});
return {
addLines,
deletedLines,
modifiedLines,
};
};
此时咱们就能够应用该函数进行一系列简略数据操作了。
const result = simpleListDiff({
newVal: [{
id: 1,
cc: "bbc",
},{bb: "123",}],
oldVal: [{
id: 1,
cc: "bb",
}, {
id: 2,
cc: "bdf",
}],
options: {
// 传入函数
getChangedItem: ({
newLine,
oldLine,
}) => {
// 利用对象比对 simpleObjDiff 来解决
const result = simpleObjDiff({
newVal: newLine,
oldVal: oldLine,
});
// 发现没有改变,返回 null
if (!Object.keys(result).length) {return null;}
// 否则返回对象比对过的数据
return {id: newLine.id, ...result};
},
key: "id",
},
});
// => 返回后果为
result = {
addedLines: [{bb: "123",}],
deletedLines: [{id: 2,}],
modifiedLines: [{
id: 1,
cc: "bbc",
}],
};
函数到这里就差不多可用了,咱们能够传入参数而后拿到比对好的后果发送给服务端进行解决。
增加默认比照函数
这里就不传递 getChangedItem 的逻辑,函数将做如下解决。如此咱们就能够不传递 getChangedItem 函数了。
const simpleListDiff = ({
newVal,
oldVal,
options,
}: SimpleObjDiffParams) => {const opts = { ...DEFAULT_OPTIONS, ...options};
// 获取以后的主键 key 数值,不传递 key 默认为 'id'
const {key} = opts;
let {getChangedItem} = opts;
// 如果没有传递 getChangedItem,就应用 simpleObjDiff 解决
if (!getChangedItem) {
getChangedItem = ({
newLine,
oldLine,
}) => {
const result = simpleObjDiff({
newVal: newLine,
oldVal: oldLine,
});
if (!Object.keys(result).length) {return null;}
return {[key]: newLine[key], ...result };
};
}
//... 之前的代码
};
增加排序功能
局部表单提交不仅仅只须要增删改,还有排序功能。这样的话即便用户没有进行过增删改,也是有可能批改程序的。此时咱们在数据中增加序号,做如下革新:
const simpleListDiff = ({
newVal,
oldVal,
options,
}: SimpleObjDiffParams) => {const opts = { ...DEFAULT_OPTIONS, ...options};
// 此时传入 sortName,不传递则不思考排序问题
const {key, sortName = ""} = opts;
// 断定是否有 sortName 这个配置项
const hasSortName: boolean = typeof sortName === "string" &&
sortName.length > 0;
let {getChangedItem} = opts;
if (!getChangedItem) {//}
const addLines = [];
const deletedLines = [];
const modifiedLines = [];
// 增加 noChangeLines
const noChangeLines = [];
const checkedKeys: Set<ListKey> = new Set<ListKey>();
newVal.forEach((newLine, index: number) => {
// 这时候须要查问老数组的索引,是利用 findIndex 而不是 find
let oldLineIndex: any = oldVal.findIndex((x) => x[key] === newLine[key]);
// 没查到
if (oldLineIndex === -1) {
addLines.push({
...newLine,
// 如果有 sortName 这个参数,咱们就增加以后序号(索引 + 1)...hasSortName && {[sortName]: index + 1 },
});
} else {
// 通过索引来获取之前的数据
const oldLine = oldVal[oldLineIndex];
// 断定是否须要增加程序参数,如果之前的索引和当初的不同就认为是扭转的
const addSortParams = hasSortName && index !== oldLineIndex;
checkedKeys.add(oldLine[key]);
const result = getChangedItem!({
newLine,
oldLine,
});
if (result !== null && result !== undefined) {
modifiedLines.push({
...result,
// 更新的数据同时增加排序信息
...addSortParams && {[sortName]: index + 1 },
});
} else {
// 这里是没有批改的数据
// 解决数据没扭转然而程序扭转的状况
if (addSortParams) {
noChangeLines.push({[key!]: newLine[key!],
[sortName]: index + 1,
});
}
}
}
});
//... 其余代码省略,删除不必思考程序了
return {
addLines,
deletedLines,
modifiedLines,
// 返回不批改的 line
...hasSortName && {noChangeLines,},
};
};
开始测试一下:
simpleListDiff({
newVal: [{ cc: "bbc"},
{id: 1, cc: "bb"}
],
oldVal: [{ id: 1, cc: "bb"}
],
options: {
key: "id",
sortName: "sortIndex",
},
});
// 同样也反对为新增和批改的数据增加 sortIndex
result = {
addedLines: [
{
cc: "bbc",
// 新增的数据目前序号为 1
sortIndex: 1,
},
],
// id 为 1 的数据地位变成了 2,然而没有产生数据的扭转
noChangeLines: [{
id: 1,
sortIndex: 2,
}],
deletedLines: [],
modifiedLines: [],};
简略数组比对函数就根本实现了。有趣味的同学也能够间接浏览 list-diff 源码。
以上所有代码都在 diff-helper 中,针对简单的服务端数据申请,能够通过传参使得两个函数可能嵌套解决。同时也欢送大家提出 issue 和 pr。
其余
针对不拘一格需要,上述两种函数解决计划也是不够用的,咱们来看看其余的比照计划。
数据递归比对
以后库也提供了一个对象或者数组的比对函数 commonDiff。能够嵌套的比对函数,能够看一下实际效果。
import {commonDiff} from "diff-helper";
commonDiff({
a: {
b: 2,
c: 2,
d: [1, 3, 4, [3333]],
},
}, {
a: {
a: 1,
b: 1,
d: [1, 2, 3, [223]],
},
});
// 以后后果均是对象,不过以后会减少 type 帮忙辨认类型
result = {
type: "obj",
a: {
type: "obj",
a: null,
b: 1,
c: 2,
d: {
type: "arr",
// 数组第 2 个数据变成了 3,第 3 数据变成了 4,以此类推
1: 3,
2: 4,
3: {
type: "arr",
0: 223,
},
},
},
};
westore 比对函数
westore 是集体应用过最好用的小程序工具,兼顾了性能和可用性。其中最为外围的则是它的比对函数,完满的解决了小程序 setData 时为了性能须要建设简单字符串的问题。
以下代码是理论的业务代码中呈现的:
// 更新表单项数据,为了性能,不倡议每次都传递一整个 user
this.setData({[`user.${name}`]: value });
// 设置数组外面某一项数据
this.setData({[`users[${index}].${name}`]: value });
这里就不介绍 westore 的用法了,间接看一下 westore diff 的参数以及后果:
const result = diff({
a: 1,
b: 2,
c: "str",
d: {e: [2, { a: 4}, 5] },
f: true,
h: [1],
g: {a: [1, 2], j: 111 },
}, {a: [],
b: "aa",
c: 3,
d: {e: [3, { a: 3}] },
f: false,
h: [1, 2],
g: {a: [1, 1, 1], i: "delete" },
k: "del",
});
// 后果
{
"a": 1,
"b": 2,
"c": "str",
"d.e[0]": 2,
"d.e[1].a": 4,
"d.e[2]": 5,
"f": true,
"h": [1],
"g.a": [1, 2],
"g.j": 111,
"g.i": null,
"k": null
}
不过这种增量比对不适宜通用场景,大家有需要能够自行查阅代码。笔者也在思考下面两个比对函数是否有其余的应用场景。
激励一下
如果你感觉这篇文章不错,心愿能够给与我一些激励,在我的 github 博客下帮忙 star 一下。
博客地址
参考资料
fast-json-stringify
westore
diff-helper