nodejs通过lodash合并去重由unixtime和Date组成的两个数组

1. 问题起源
最近在实现一个API,其中有一部分功能是需要从Mongodb中取出一个由Date对象组成的数组,然后将客户端传过来的unixtime合并到该数组中,并且去重复。
比如,假设从mongodb中取回来的数据中有一个叫做gaming的项,专门用来记录用户进入游戏的开始时间和退出时间。 那么mongoose的schema的定义将大概是这样的:

const DeviceLogSchema = new Schema({ ... gaming: [{ enter: Date, exit: Date, _id: false }], ... });

而从mongodb取回来的数据大概就是这个样子的:
{ "gaming": [ { "enter": 2017-04-25T14:32:12.081Z, "exit": 2017-04-25T14:48:52.082Z, }, { "enter": 2017-04-26T14:32:12.081Z, "exit": 2017-04-26T14:48:52.082Z, } ], }

也就是说通过mongoose的model取回来的记录中,enter和exit都是Date(对mongodb来说)类型的,而对于js来说,就是一个Object(js将所有简单类型以外的数据类型都处理成Object)。
let deviceLog = await DeviceLog.findOne({}); console.log(typeof deviceLog.enter) // ->Object

而客户端每隔一段时间就会调用api来将新的用户游戏时间回传给服务器,但用的格式是unixtime。
{ "gaming": [{ "enter": 1493130733081, "exit": 1493131734082, },{ "enter": 1493130735084, "exit": 1493131736087, }], }

这里的enter和exit的unixtime时间格式,对于js来说,就是number类型的。
我们通过mongoose来保存的时候,不需要将unixtime进行任何转换,直接保存, mongoose会将其自动转成Date格式进行保存。也就是说,如果保存前的gaming内容是如下这个样子的:
"gaming": [ { "enter": 2017-04-25T14:32:12.081Z, "exit": 2017-04-25T14:48:52.082Z, }, { "enter": 2017-04-26T14:32:12.081Z, "exit": 2017-04-26T14:48:52.082Z, }, { "enter": 1493130733081, "exit": 1493131734082, }, { "enter": 1493130735084, "exit": 1493131736087, } ],

那么通过mongoose的model保存之后,最终会自动成为类似以下这样的格式:
"gaming": [ { "enter": 2017-04-25T14:32:12.081Z, "exit": 2017-04-25T14:48:52.082Z, }, { "enter": 2017-04-26T14:32:12.081Z, "exit": 2017-04-26T14:48:52.082Z, }, { "enter": 2017-04-27T14:22:12.021Z, "exit": 2017-04-27T15:32:12.031Z, }, { "enter": 2017-04-26T16:22:12.082Z, "exit": 2017-04-26T16:52:12.082Z, } ],

那么这里要解决的问题就是:
  • 如何将客户端传过来的新数组和从mongodb取回来的数组进行合并
  • 合并时如何根据游戏进入的时间enter来去重复
【nodejs通过lodash合并去重由unixtime和Date组成的两个数组】当然,我们可以用原始的方法,做两层遍历,分别便利两个不同的数组,然后将其中一个比对数据的类型转换成另外一个数据对应的类型,然后进行比较其是否相等,相等的话就去掉,不想等的话就将数据追加到数组中。
但,这样效率太低了,应该有更好的更优雅的方法来帮助我们处理这种问题。
2. 实验数据
那么我们就根据上面碰到的问题,来建立两个实验所用的数据。一个是代表从mongodb取回来的数据:
const orgGaming = [ { "enter": new Date("2017-04-25T14:32:12.081Z"), "exit": new Date("2017-04-25T14:48:52.082Z"), }, { "enter": new Date("2017-04-26T14:32:12.081Z"), "exit": new Date("2017-04-26T14:48:52.082Z"), } ]

一个是客户端传进来的数据:
const newGaming = [ { "enter": 1493130732081, // 这和orgGamine第一条数据重复 "exit": 1493131732082, // 这和orgGamine第一条数据重复 }, { "enter": 1493130735084, "exit": 1493131736087, } ]

新数组中的第一条数据和enter和数据库中的第一条数据的enter,事实上是相同的,所以我们希望合并之后这个重复数据是去掉的。
3. ES6数组合并新特性
其实,如果不是因为要考虑去重复的问题的话,我们完全可以通过ES6的新特性来完成的。
array1.push(...array2)

这里的’…’操作符叫做扩展运算符,是ES6引入的新特性。目的是将一个数组打散成用逗号分隔的参数序列。
const array = [1, 2]; console.log(...array); // 相当于 console.log(1,2)

所以上面的示例代码的意思就是将array2打散后,将每个元素作为参数push到array1中生成新的数组。所以,如果应用到我们的场景中的话
const orgGaming = [ { "enter": new Date("2017-04-25T14:32:12.081Z"), "exit": new Date("2017-04-25T14:48:52.082Z"), }, { "enter": new Date("2017-04-26T14:32:12.081Z"), "exit": new Date("2017-04-26T14:48:52.082Z"), } ]const newGaming = [ { "enter": 1493130732081, "exit": 1493131732082, }, { "enter": 1493130735084, "exit": 1493131736087, } ]orgGaming.push(...newGaming); console.log(orgGaming);

最终将会输出没有去重复的结果:
[ { enter: 2017-04-25T14:32:12.081Z, exit: 2017-04-25T14:48:52.082Z }, { enter: 2017-04-26T14:32:12.081Z, exit: 2017-04-26T14:48:52.082Z }, { enter: 1493130732081, exit: 1493131732082 }, { enter: 1493130735084, exit: 1493131736087 } ]

当然,ES6的这个数组合并方式还可以这样写:
[...array1,...array2]

同时,ES6还提供了对简单数据类型去重复方式:
[...new Set([...array1 ,...array2])];

但是,这个只能针对简单数据类型进行去重复,比如数字类型和字串类型等。
const array1 = ['techgogogo', 'sina', 'baidu']; const array2 = ['techgogogo', 'google']; console.log([... new Set([...array1, ...array2])]);

最后输出:
[ 'techgogogo', 'sina', 'baidu', 'google' ]

但是对于我们这里的对象类型组成的数组,它是做不到的。
最重要的是,它没有提供一个comparator的回调方法来放我们处理应该如何判断,两个数据是否是重复的。
这里,lodash的数组操作,也许是个正确的解决方案(之一)。
4. lodash合并对象类型数组并去重复
lodash的unionWith方式可以合并两个数组,并且可以让我们提供一个comparator方法来控制该如何比较两个数组中的元素是否是一致的,以此来判断这个数据是否是重复的。
官方文档对unionWith方法的描述请看这里:https://lodash.com/docs/4.17.4#unionWith
_.unionWith([arrays], [comparator])

理解起来也比较简单,请看代码如下:
const _ = require('lodash'); const orgGaming = [ { "enter": new Date("2017-04-25T14:32:12.081Z"), "exit": new Date("2017-04-25T14:48:52.082Z"), }, { "enter": new Date("2017-04-26T14:32:12.081Z"), "exit": new Date("2017-04-26T14:48:52.082Z"), } ]const newGaming = [ { "enter": 1493130732081, "exit": 1493131732082, }, { "enter": 1493130735084, "exit": 1493131736087, } ]gaming = _.unionWith(orgGaming, newGaming, (value1, value2) => { if (typeof value1.enter === 'number' && typeof value2.enter === 'number') { return (value1.enter === value2.enter); } else if (typeof value1.enter === 'number' && typeof value2.enter === 'object') { return (value1.enter === value2.enter.getTime()); } else if (typeof value1.enter === 'object' && typeof value2.enter === 'number') { return (value1.enter.getTime() === value2.enter); } else if (typeof value1.enter === 'object' && typeof value2.enter === 'object') { return (value1.enter.getTime() === value2.enter.getTime()); } }); console.log(gaming);

这里关键的地方就是uionWith,有几个地方需要注意:
  • 参数的顺序,特别是前两个数组参数。如果第一个数组中某个成员判定和第二个数组中的某个成员是重复的,那么第一个数组中的该元素会保留,第二个数组中的对应元素会移除。
  • 第三个参数就是一个回调方法,接受两个参数,其实就是两个需要比对的数组的成员,这里我们通过比对两个成员的enter是否相等来判断该成员是否重复。
  • 判断是否重复的时候,我们需要将日记先转换成unixtime的格式,然后再进行比较。
最终我们可以看到去重复后的输出:
[ { enter: 2017-04-25T14:32:12.081Z, exit: 2017-04-25T14:48:52.082Z }, { enter: 2017-04-26T14:32:12.081Z, exit: 2017-04-26T14:48:52.082Z }, { enter: 1493130735084, exit: 1493131736087 } ]

可以看到,最后输出的列表中只有三个对象,其中一个重复的已经被摒弃掉了。
最后我们通过mongoose将这份数据存储到mongodb时,如前面所述,会自动将unixtime转换成Date进行存储,这样数据就统一起来了。这里mongoose的操作就不赘述了,有兴趣的朋友可以自己实践下。
以上就是本人对两个由对象类型组成的数组进行合并的一些尝试和实践,如果大家有更好更优雅的方式的话,欢迎在评论中给出来。先拜谢了!
本文由天地会珠海分舵编写,转载需授权,喜欢点个赞,吐槽请评论,进一步交流请关注公众号techgogogo或者直接联系本人微信zhubaitian1

    推荐阅读