在开发 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",  },});// 同样也反对为新增和批改的数据增加 sortIndexresult = {  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 时为了性能须要建设简单字符串的问题。

以下代码是理论的业务代码中呈现的:

// 更新表单项数据,为了性能,不倡议每次都传递一整个 userthis.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