JSON.stringify()的性能要如何提升?
在一些性能敏感的情况下(比如服务器处理大量并发时)或者面临大量的stringify操作,我们希望它的性能更好更快。这也催生了一些优化的 stringify 解决方案/库。下图是它们和原生方法的性能对比:
绿色部分是原生的JSON.stringify()
。可以看出性能比这些库要高。低得多。那么性能显着提升背后的技术原理是什么呢?
2。比 stringify
stringify
由于 JavaScript 是一种非常动态的语言,对于一个对象类型变量来说,它包含了键名、键值。键值类型最终仅在运行时确定。因此,当您执行 JSON.stringify()
时,还有很多工作要做。在不了解其他情况的情况下,我们无法采取任何措施来显着优化。
那么如果我们知道这个对象的键名和键值信息——也就是知道它的结构信息,这会有用吗?
看一个例子:
下面的对象,
const obj = {
name: 'alienzhou',
status: 6,
working: true
};
我们对它应用 所以实际上我可以创建一个“自定义”字符串方法❀看看我们的❀myStringify输出JSON.stringify()
方法:,结果是如果我们知道这个N obj 的结构 是固定:
myStringify({
name: 'alienzhou',
status: 6,
working: true
});
// {"name":"alienzhou","status":6,"isWorking":true}
myStringify({
name: 'mengshou',
status: 3,
working: false
});
// {"name":"mengshou","status":3,"isWorking":false}
可以得到正确的结果,但只使用了类型转换和字符串连接,因此“自定义”方法可以使“stringify”更快。
如何获得比stringify
更快的stringify
方法?
- 首先要确定物体的结构信息;
- 根据其结构信息为此结构中的对象创建“自定义”
stringify
方法。内部结果实际上是通过字符串连接生成的。 - 最后使用这个“自定义”方法来串接对象。 ?仅供参考,您需要为这种类型的对象创建一个“自定义” stringify 方法,这实际上是简单的属性访问和字符串连接。
为了了解具体的实现方法,下面我以两个实现稍有不同的开源库为例简单介绍一下。
3.1。 fast-json-stringify
下图是根据fast-json-stringify提供的benchmark结果进行的性能对比。
如您所见,在大多数场景下它都有 2-5 倍的性能提升。
3.1.1。如何定义模式
fast-json-stringify 使用 JSON 模式验证来定义 (JSON) 对象的数据格式。模式本身定义的结构也是 JSON 格式。例如,对象
{ name: 'alienzhou', status: 6, working: true }
对应的模式为:
{ title: 'Example Schema', type: 'object', properties: { name: { type: 'string' }, status: { type: 'integer' }, working: { type: 'boolean' } } }
。其模式定义规则丰富。具体使用可以参考JSON验证库Ajv。
3.1.2。生成stringify方法
fast-json-stringify会根据刚刚定义的schema生成实际的函数代码字符串,然后使用Function构造函数在运行时动态生成相应的stringify函数。
在代码生成方面,主要会注入各种预定义的工具方法。这部分的各个schema都是一样的:
var code = ` 'use strict' ` code += ` ${$asString.toString()} ${$asStringNullable.toString()} ${$asStringSmall.toString()} ${$asNumber.toString()} ${$asNumberNullable.toString()} ${$asIntegerNullable.toString()} ${$asNull.toString()} ${$asBoolean.toString()} ${$asBooleanNullable.toString()} `
其次,会根据schema定义的具体内容生成stringify函数的具体代码。 。生成方法也比较简单:通过遍历schema。
单步执行模式时,根据定义的类型,在相应的键值转换代码处插入相应的实用函数。例如,在上例中,属性
name
为:var accessor = key.indexOf('[') === 0 ? sanitizeKey(key) : `['${sanitizeKey(key)}']` switch (type) { case 'null': code += ` json += $asNull() ` break case 'string': code += nullable ? `json += obj${accessor} === null ? null : $asString(obj${accessor})` : `json += $asString(obj${accessor})` break case 'integer': code += nullable ? `json += obj${accessor} === null ? null : $asInteger(obj${accessor})` : `json += $asInteger(obj${accessor})` break ……
上面代码中的变量
code
存储的是最后生成的函数文本的代码字符串。由于模式定义中name
的类型为string
并且不为空,因此以下代码字符串将添加到codecode到:也需要处理复杂的情况例如数组和链接对象,所以省略了很多实际代码。
之后,整个生成的
code
字符串看起来像这样:function $asString(str) { // …… } function $asStringNullable(str) { // …… } function $asStringSmall(str) { // …… } function $asNumber(i) { // …… } function $asNumberNullable(i) { // …… } /* 以上是一系列通用的键值转换方法 */ /* $main 就是 stringify 的主体函数 */ function $main(input) { var obj = typeof input.toJSON === 'function' ? input.toJSON() : input var json = '{' var addComma = false if (obj['name'] !== undefined) { if (addComma) { json += ',' } addComma = true json += '"name":' json += $asString(obj['name']) } // …… 其他属性(status、working)的拼接 json += '}' return json } return $main
最后将
code
code❀字符串传递给函数❀或构造字符串。
// dependencies 主要用于处理包含 anyOf 与 if 语法的情况 dependenciesName.push(code) return (Function.apply(null, dependenciesName).apply(null, dependencies))
3.2。 Slow-json-stringify
slow-json-stringify 虽然名字很“慢”,但它实际上是一个“快”的 stringify 库(名字很厚颜无耻)。
已知宇宙中最慢的弦化器。开个玩笑,它是最快的(:
它的实现比前面提到的fast-json-stringify更加轻量级,思路也很聪明,同时在很多场景下使用,效率会更快比 fast-json-stringify .
3.2.1 如何定义 schema
Slow-json-stringify 的 schema 定义更加自然简单,主要是将 key value 替换为类型描述。还是在上面object 的例子中,schema 就变成了
{ name: 'string', status: 'number', working: 'boolean' }
,其实很直观。这样定义好之后,我们就可以先修改一下 schema
JSON.stringify
,然后“减去”所有的 type 值。最后,什么等待我们的就是将实际填写的Value直接放入scheme对应的类型声明中。如何操作?
首先可以直接在schema上调用
JSON.stringify()
生成基本模板,借用JSON.stringify()❀方法的第二个参数来采集属性。访问路径:
let map = {}; const str = JSON.stringify(schema, (prop, value) => { const isArray = Array.isArray(value); if (typeof value !== 'object' || isArray) { if (isArray) { const current = value[0]; arrais.set(prop, current); } _validator(value); map[prop] = _deepPath(schema, prop); props += `"${prop}"|`; } return value; });
此时,
map
收集了所有属性的访问路径。同时生成的props
可以拼接成匹配对应类型字符的正则表达式。例如,我们示例中的正则表达式为/name|status|working"(string|number|boolean| undef)"|\\[(.*?)\\]/
。然后根据正则表达式依次匹配这些属性,将属性类型字符串替换为总占位符字符串
"__par__"
并基于"字符串:这样就得到两个
chunks
和props
的数组。chunks
包含共享的JSON字符串。例如,这两个数组如下// chunks [ '{"name":"', '","status":"', '","working":"', '"}' ] // props [ 'name', 'status', 'working' ]
最后,由于映射存储了协会属性名和访问路径,可以根据prop访问对象中的某个属性的值,循环遍历数组,与对应的块进行拼接即可。
从代码角度体积和实现方法上,这种方案会更轻量、更巧妙,而且不需要通过Function、eval等动态生成或执行函数。
4.总结
虽然不同库的实现不同,实现高性能stringify的总体思路是相同的:
- 开发者为Object定义JSON schema;该库根据模式生成相应的模板方法。模板方法中,属性和值都是字符串拼接的(显然,属性访问和字符串拼接效率高很多);
- 最后开发者调用返回的方法对Object进行stringify就可以了。
归根结底,它本质上是通过静态结构信息来预测优化和分析。
提示
最后我还是要提一下
- 所有基准测试只能作为参考。建议您在实际业务中测试是否有性能提升以及提升了多少;
- fast-json - stringify 中使用了函数构造函数,因此不建议直接使用用户输入作为 schema,以防止一些安全问题。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。