|
假设你需要在前端展示 5000 条甚至更多的数据,每一条数据的数据结构是一个对象,里面有各种各样的属性。每个属性的值又可以是基本类型,对象,甚至数组。这里的对象或者数组内部的元素又可以继续包含对象或者数组并且允许无限嵌套下去。比如
{ "name": { "firstName": "yi", "lastName": "li" }, "age": 23, "roles": ['developer', 'admin'], "projects": [{ "name": "demo", "repo": "" }]
}
页面上提供一个搜索框,用户通过输入搜索的内容可以找到包含这个内容的数据。注意,只要任意数据对象的任意属性值 (比如在上面的数据结构中,只要 name, age, roles 任何一个属性的值)包含这个关键词即可。如果属性值是数组或者对象,那么数组的元素或者对象的值继续对输入内容进行匹配检测,并递归的检测下去,只要有命中,便算该数据匹配
如何设计这个功能,让搜索功能尽可能的快?
解决思路
如果你稍有程序员的敏感度,此时你的脑海里应该有两个念头:
- 遍历以及深度优先遍历是最直接的方式
- 如果要求够快的话遍历我就输了
的确,遍历是最简单但也是最慢的。所以通常的优化方法之一是通过空间换取时间;而另一个方法……稍后再引出。
这里我们尝试通过建立字典树(Trie)来优化搜索。
如果你还不了解什么是字典树,下面做简单的介绍:假设我们有一个简单的对象,键值的对应关系如下:

我们根据「键」的字母出现顺次构建出一棵树出来,叶子节点值即有可能是某个「键」的值

那么此时无论用户想访问任何属性的值,只要从树的根节点出发,依据属性字母出现的顺序访问树的叶子节点,即可得到该属性的值。比如当我们想访问tea时:

但是在我们需要解决的场景中,我们不需要关心「属性」,我们只关心「值」是否匹配上搜索的内容。所以我们只需要对「值」建立字典树。
假设有以下的对象值
const o = {
message: 'ack',
fruit: 'apple',
unit: 'an',
name: 'anna',
}
建立的树状结构如下:
root--a |--c |--k |--p |--p |--l |--e |--n |--n |--a
当用户搜索 apple 时,从a开始访问,至最后访问到字母 e 时,若在树中有对应的节点,表示命中;当用户搜索 aha 时,在访问 h 时就已经无法在树中找到对应的节点了,表示该对象不符合搜索条件
但实际工作中我们会有非常多个对象值,多个对象值之间可能有重复的值,所以匹配时,我们要把所有可能的匹配结果都返回。比如
[
{
id: 1,
message: 'ack',
fruit: 'apple',
unit: 'an',
name: 'anna',
},
{
id: 2,
message:'ack',
fruit: 'banana',
unit: 'an',
name: 'lee',
},
]
上面两个对象有相同的值 ack 和 an,所以在树上的叶子节点中我们还要添加对象的 id 辨识信息
root--a |--c |--k (ids: [1,2]) |--p |--p |--l |--e (ids: [1]) |--n (ids: [1, 2]) |--n |--a (ids: [1])
这样当用户搜索 an 时,我们能返回所有的匹配项
OK,有了思路之后我们开始实现代码。
代码实现
假数据
首先要解决的一个问题是如果快速的伪造 5000 条数据?这里我们使用 https://randomqmtйmйmɑ(й(%%й(tСФt?r'%%gZWRRn>C*
/&r'jC*
kbn7N724Os_27(Bs*{>+_GjBs*{>+_Gj(皾SV#:WBszsjGrgj7:jBsZTń(-ItmM-II)?rWB27Bs7^G]3bbn_Gjr>7Bs7j4Bs44皾PG](皾P皾P皾Szsb?wjń(6?^&:'j6?^SvjOBs7v{~S_Gj~&:S7:Bsb>73*j^^VňOBs7>cS_Gj~&:S7:Bs`(V#:>73^cj"bńOs76W^>C*
k>>gN77:j6R"^^("Gr"j~r"GJ#grkX(c2[~Bsjr2[~Bsjrn{6WBsjrN#r"Gr7:>C*
/j&r'C*
Gz3B;7>GR>c2X"G>r>C&7_>C*
&r'@gbZj[Z皆 _("GgZjZTK?>C*
&r'C*
(]
%Ф(й%%%йm%%%((]
% |