如何在不冻结DOM的情况下使用JavaScript加载非常大的字典?

3
我写了一个拼写检查脚本,使用了一个很长的单词列表。我将这个庞大的单词列表格式化为JavaScript对象,并将其作为脚本加载。因此,当它被加载时,那个非常大的对象就被解析了。 dictionary.js
var dictionary = {
   "apple" : "apple",
   "banana" : "banana",
   "orange" : "orange"
}

为了进行即时单词有效性检查,按这种方式格式化:

if (dictionary["apple"]){
    //word is valid
}

问题在于解析这个巨大的对象会导致DOM冻结。

我该如何以逐步解析的方式加载数据结构? 我该如何存储/加载我的数据结构,使得DOM可以处理它而不会冻结?


1
也许考虑按字母或其他方式分隔列表。 - techfoobar
1
@Leandro 我非常清楚如何在 Stack Overflow 上提问。你觉得这个问题的提问方式有问题吗? - user4231564
是的,我确实评论了,因为我认为这是一个好问题,但有人给你投了反对票,我认为这是因为没有人回答你的同样原因。在这里,人们更喜欢至少有一次尝试的问题,像“做我的家庭作业”或没有研究的问题不受欢迎。 - Leandro Bardelli
2
@Leandro 我明白.. 不过,我真的不确定从哪里开始,有时这就是问题的情况.. - user4231564
2
你考虑过使用Web Workers吗?https://developer.mozilla.org/zh-CN/docs/Web/Guide/Performance/Using_web_workers - demux
显示剩余7条评论
2个回答

6

以以下格式编写JS文件

var words = JSON.parse('{"aardvark": "aardvark", ...}');

JSON解析的速度比JS解析器快几个数量级。

根据我的测量,实际查找大约需要0.01毫秒。

背景

在考虑此情况下的性能时,有几个方面需要考虑,包括下载带宽、解析、预处理或构建(如果需要)、内存和检索。在这种情况下,所有其他性能问题都被JS解析时间所压倒,对于120K条目哈希表,解析时间可能高达10秒。如果从服务器下载,2.5MB的大小可能是一个问题,但是这将缩小到原始大小的20%左右。就检索性能而言,JS哈希已经针对快速检索进行了优化;实际检索时间可能少于0.01毫秒,特别是对于同一键的后续访问。在内存方面,似乎很少有优化的方法,但大多数浏览器可以轻松地容纳这么大的对象。

Patricia trie方法很有趣,主要解决内存使用和下载带宽问题,但在这种情况下,它们似乎不是主要的问题领域。


为了回答你的问题和疑惑,这个JSFiddle将会演示:http://jsfiddle.net/jdnmy9e8/1/,并且字典以两种对象格式被加载,这意味着大约240K个单词(有一个复杂的原因),但它们都是通过JQuery异步加载的。一个解决方案是构建自己的模糊字符串搜索函数,以避免需要单独的字典格式进行拼写检查和拼写*建议*。但是120K的大小仍然会导致DOM冻结约1秒钟。至少在我的系统上,在Chrome中是这样的。 - user4231564
这非常有趣。JSON.parse比本地解析快得多,仅仅因为JSON是有效JS的一个小子集吗?此外,这是否意味着如果您使用经过eval填充的JSON.parse版本,则性能增益将消失? - Chris Middleton
是的,它是这样的,而且是的,它会这样。 - user663031
Amadeus的patricia树解决方案确实可以节省3倍的列表大小,但查找速度会降低1/3。我将采用自己版本的答案,该答案建议将其解析为JSON,但我接受此答案。基本上,如果您已经要进行模糊字符串搜索以修正拼写,则使用patricia树是有意义的,但是对于此问题,如果您只想加载字典,则应该选择这种方法。 - user4231564
测试后:js通常解析的对象解析需要195毫秒。而且,如此答案中所建议的,以JSON格式解析速度更快,只需126毫秒。结果会因机器而异,但相对性通常不会改变。 - user4231564
我简直不敢相信这有多快。 - jason m

3
如果你的列表中有很多单词,为了缩小字典的大小,你可以使用Patricia Trie。Patricia Trie是一种专门优化搜索字符串的树形结构。基本上,Trie中的每个节点都是一个单独的字母。例如:
var dictionary = {
    'a': {
        '': 1, // a
        'a': {
            '': 1, // aa
            'l': {
                '': 1, // aal
                'ii': 1 // aalii
            },
            'm': 1, // aam
            'ni': 1, // aani
            'r': {
                'd': {
                    'vark': 1, // aardvark
                    'wolf': 1 // aardwolf
                },
                'on': {
                    '': 1, // aaron
                    'ic': 1 // aaronic
                }
            }
        },
    },
}

在上面的代码中,我使用空字符串 ''(实际上是有效的属性标识符!)来表示单词的结尾。
以下是如何实现搜索:
function isWord_internal(str, dic) {
    var strlen, i, substr, substrlen;
    strlen = str.length;
    for(i = strlen; i > 0; i--) {
        substr = str.slice(0, i);
        substrlen = substr.length;
        if(dic[substr]) {
            if(dic[substr] === 1) {
                if(substrlen === strlen) {
                    return true;
                }
                return false; // end of the line, folks
            }
            if(dic[substr][''] && substrlen === strlen) {
                return true;
            }
            return isWord_internal(str.slice(i), dic[substr]);
        } // else keep going
    }
    // not found
    return false;
}

function isWord(str) {
    return isWord_internal(str, dictionary); // assumes that the dictionary variable exists already
}

由于对象是哈希表 O(log n),而算法与单词大小线性相关,因此您的性能应为 O(n log n)。

这也应该使您的列表大小保持不变,因为英语单词不是随机的,通常具有共同的子字符串(虽然您可能需要将其最小化)。

要转换您当前的字典,您可以使用此函数在内存中生成它:

function patriciafy_internal(str, pat) {
    var i, substr, patkeys, patkeyslen, j, patkey, pos, portion;
    for(i = str.length; i > 0; i--) {
        substr = str.slice(0, i);
        patkeys = Object.keys(pat);
        patkeyslen = patkeys.length;
        for(j = 0; j < patkeyslen; j++) {
            patkey = patkeys[j];
            pos = patkey.indexOf(substr);
            if(pos !== -1) {
                if(pat[patkey] !== 1) {
                    // keep going down the rabbit hole
                    patriciafy_internal(str.slice(i), pat[patkey]);
                } else {
                    // split this node and store the new key
                    portion = patkey.slice(0, pos + 1);
                    delete pat[patkey];
                    pat[portion] = {'': 1};
                    pat[portion][str.slice(1)] = 1;
                }
                return;
            }
        }
    }
    // no substring found - store the whole thing at this level
    pat[str] = 1;
}

function patriciafy(dic) {
    var pat, keys, keyslen, str, i;
    pat = {};
    keys = Object.keys(dic);
    keyslen = keys.length;
    for(i = 0; i < keyslen; i++) {
        // insert this str into the trie
        str = keys[i];
        patriciafy_internal(str, pat);
    }
    return pat;
}

然后只需使用JSON.stringify将其打印出来以供使用。 在我的机器上进行的一项测试,使用235,887个单词的字典(从here中获取),表明Patricia树比单词的平面字典小了约三分之一,其中平面字典的格式为{"name":1,"name2":1,...}

Patricia tree: 2,296,106 characters, Flat dictionary: 3,419,872 characters

这仍然很大,但明显小了很多。你可以通过将其存储在非JS格式中来节省相当大的空间,例如。
var dictionary = {
    'a': {
        '': 1,
        'a': {
            '': 1,
            'l': {
                '': 1,
                'ii': 1
            }
        }
    }
};

压缩后变成

// 57 characters
var dictionary={'a':{'':1,'a':{'':1,'l':{'':1,'ii':1}}}};

可以变成

// 17 characters - single @ represents ''
@a{@@a{@@l{@@i}}}

然后你将解析它。

=====

patricia树到@语法:

function patToAt_internal(pat) {
    var keys, i, s = "", key;
    s += "{";
    keys = Object.keys(pat);
    for(i = 0; i < keys.length; i++) {
        key = keys[i];
        s += "@";
        s += key; // empty string will do nothing
        if(pat[key] !== 1) { // concat the sub-trie
            s += "{" + patToAt_internal(pat[key]) + "}";
        }
    }
    s += "}";
    return s;
}

function patToAt(pat) {
    return patToAt_internal(pat);
}

@-语法转换为 Patricia 树:

function atToPat_internal(at, pos) {

    var s = "", result, ff = true, ch;
    s += "{";
    pos++; // eat { character
    while((ch = at.charAt(pos)) !== "}") {
        // eat @ character
        pos++;
        // comma
        if(!ff) s += ",";
        else ff = false;
        // start key quote
        s += '"';
        // get the key
        while((ch = at.charAt(pos)) !== "@" && ch !== "{" && ch !== "}") {
            s += ch;
            pos++;
        }
        // close key quote and colon
        s += '":';
        // sub-trie
        if(ch === "{") {
            result = atToPat_internal(at, pos);
            s += result.s;
            pos = result.pos + 1;
        } else {
            s += "1";
        }
    }
    s += "}";
    return {pos: pos, s: s};

}

// this part is difficult, so we'll just delegate the heavy lifting to JSON.parse
function atToPat(at) {
    return JSON.parse(atToPat_internal(at, 0).s);
}

使用@语法,总大小缩小到原始平面字典的三分之一:

enter image description here

总之...

使用这种方法,你需要最初(仅一次)通过执行以下步骤来构建 @-tree。

var dictionary = { ... } // your words here
// usually you can get all of the log if you triple click, or just output it into a textarea
console.log(patToAt(patriciafy(dictionary)));

然后,一旦你完成了这个步骤,在你的页面上,你可以执行以下操作:

var dictionary = atToPat("..."); // @-tree goes in the quotes

// and look up things like this:
isWord('apple');

“get all of the words” 是什么意思? - Chris Middleton
是的...我只是在寻找一种自动化处理的方法。这有点棘手,但如果我想出来了,我会告诉你的。顺便说一句,好问题 - 很遗憾你最初被投票否决了。不经常看到实际上有些新鲜的问题。 - Chris Middleton
好的,你的标题还可以。更合适的答案应该是通过AJAX调用逐步获取字典的片段,但我认为这样做性能不太好,所以我给出了这个答案。实际上是我的回答有些偏离问题,而不是你的问题有问题。但如果你愿意,你可以把它改成类似“如何更有效地存储字典?”的问题。 - Chris Middleton
我刚刚在这里尝试了一个超过200K的列表:https://raw.githubusercontent.com/eneko/data-repository/master/data/words.txt,Patricia树大约要大1.5倍。哦,好吧。我认为存储字符串的更长片段可能会使其工作,但编码难度更大。通过AJAX调用逐步拉入片段和/或像Arnar在评论中建议的那样使用可能是您最好的选择。随意取消选中此答案。我现在将其保留下来,以防我想到如何保存它。 - Chris Middleton
请将此答案留在这里;通过模糊字符串搜索获取单词建议,这种方法可能更好。因此,我可能会使用它而不是其他方法。http://en.wikipedia.org/wiki/Approximate_string_matching - user4231564
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接