来源 |
1.熟悉的JSON.()
在浏览器端或者服务器端,JSON.()是一个非常常用的方法:
在一些性能敏感的情况下(例如服务器处理大量并发),或者面对大量操作时,我们希望它的性能更好,速度更快。 这也导致了一些优化的解决方案/库。 下图展示了它们与原生方法的性能对比:
绿色部分是原生的JSON.(),可见性能比这些库低很多。 那么,性能大幅提升背后的技术原理是什么呢?
2. 比
由于它是一种高度动态的语言,对于一种类型的变量,其包含的键名、键值、键值类型只能在运行时确定。 所以执行 JSON.() 的时候还有很多工作要做。 在不了解任何情况的情况下,如果我们想要大幅优化,显然无能为力。
那么如果我们知道这里面的键名和键值信息——也就是知道它的结构信息,这会有帮助吗?
看一个例子:
在此之下,
const obj = {
name: 'alienzhou',
status: 6,
working: true
};
我们对其应用 JSON.() ,结果是
JSON.stringify(obj);
// {"name":"alienzhou","status":6,"working":true}
现在如果我们知道这个 obj 的结构是固定的:
好吧,实际上,我可以创建一个“自定义”方法
function myStringify(o) {
return (
'{"name":"'
+ o.name
+ '","status":'
+ o.status
+ ',"isWorking":'
+ o.working
+ '}'
);
}
看一下我们方法的输出:
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}
可以得到正确的结果,但只用到了类型转换和字符串拼接,所以“自定义”的方法可以让“”更快。
综上所述,如何获得比
这也是大多数加速库的套路,代码类似:
import faster from 'some_library_faster_stringify';
// 1. 通过相应规则,定义你的对象结构
const theObjectScheme = {
// ……
};
// 2. 根据结构,得到一个定制化的方法
const stringify = faster(theObjectScheme);
// 3. 调用方法,快速 stringify
const target = {
// ……
};
stringify(target);
3. 如何生成“自定义”方法
根据上面的分析,其核心功能就是根据该类对象的结构信息为其创建一个“定制”的方法,其内部其实就是简单的属性访问和字符串拼接。
为了便于理解具体的实现,我简单介绍一下两个实现稍有不同的开源库作为例子。
3.1. 快速-json-
下图是根据fast-json-提供的结果进行的性能对比。
可以看到,在大多数场景下,都有2-5倍的性能提升。
3.1.1. 如何定义
fast-json - 使用JSON来定义(JSON)对象的数据格式。它本身定义的结构也是JSON格式的,比如对象
{
name: 'alienzhou',
status: 6,
working: true
}
对应的是:
{
title: 'Example Schema',
type: 'object',
properties: {
name: {
type: 'string'
},
status: {
type: 'integer'
},
working: {
type: 'boolean'
}
}
}
其定义规则丰富。 具体使用请参考Ajv,一个JSON验证库。
3.1.2. 方法生成
fast-json-会根据刚才的定义生成实际的函数代码字符串,然后在运行时使用构造函数动态生成对应的函数。
在代码生成方面,首先它会注入各种预定义的工具方法,这些方法都是一样的:
var code = `
'use strict'
`
code += `
${$asString.toString()}
${$asStringNullable.toString()}
${$asStringSmall.toString()}
${$asNumber.toString()}
${$asNumberNullable.toString()}
${$asIntegerNullable.toString()}
${$asNull.toString()}
${$asBoolean.toString()}
${$asBooleanNullable.toString()}
`
其次,会根据定义的具体内容生成函数的具体代码。 而且生成的方式也比较简单:通过遍历。
遍历时,根据定义的类型,在对应的代码处插入对应的工具函数,进行键值转换。 例如上例中的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是一个类型并且不为空,因此将在代码中添加以下代码字符串:
"json += $asString(obj['name'])"
由于需要处理数组、链接对象等复杂情况,所以省略了很多实际代码。
那么生成的完整代码串大致如下:
function $asString(str) {
// ……
}
function $asStringNullable(str) {
// ……
}
function $asStringSmall(str) {
// ……
}
function $asNumber(i) {
// ……
}
function $asNumberNullable(i) {
// ……
}
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
最后,将代码字符串传递到构造函数中以创建相应的函数。
// dependencies 主要用于处理包含 anyOf 与 if 语法的情况
dependenciesName.push(code)
return (Function.apply(null, dependenciesName).apply(null, dependencies))
3.2. 慢-json-
Slow-json——虽然名字是“慢”,但它实际上是一个“快”的库(名字很调皮)。
众所周知。 只是,它是 (:
它的实现比前面提到的fast-json-更加轻量级,而且想法也非常巧妙。 同时,在很多场景下它的效率会比fast-json-更快。
3.2.1. 如何定义
Slow-json-的定义更加自然简单,主要是将键值替换为类型描述。还是上面对象的例子,就会变成
{
name: 'string',
status: 'number',
working: 'boolean'
}
确实非常直观。
3.2.2. 方法生成
不知道你注意到了吗
// scheme
{
name: 'string',
status: 'number',
working: 'boolean'
}
// 目标对象
{
name: 'alienzhou',
status: 6,
working: true
}
是不是和原来物体的结构很相似?
这样做的巧妙之处在于,在这个定义之后,我们可以把 . 首先,然后“扣除”所有类型值,最后等待我们的就是将实际值直接填充到相应的类型声明中。
怎么做?
首先可以直接调用JSON.()生成基础模板,借用JSON.()的第二个参数作为遍历方法的访问路径来收集属性:
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||"(|||undef)"|\\[(.*?)\\ ]/。
然后,根据正则表达式按顺序匹配这些属性,将属性类型的字符串替换为统一的占位符字符串“”,并根据“”分割字符串:
const queue = [];
const chunks = str
.replace(regex, (type) => {
switch (type) {
case '"string"':
case '"undefined"':
return '"__par__"';
case '"number"':
case '"boolean"':
case '["array-simple"]':
case '[null]':
return '__par__';
default:
const prop = type.match(/(?<=\").+?(?=\")/)[0];
queue.push(prop);
return type;
}
})
.split('__par__');
这样你就会得到两个数组和道具。 包含分割后的JSON字符串。以两个数组为例,如下
// chunks
[
'{"name":"',
'","status":"',
'","working":"',
'"}'
]
// props
[
'name',
'status',
'working'
]
最后,由于map中保存了属性名称与访问路径的映射关系,因此可以根据prop访问对象中的某个属性的值,循环遍历数组,与对应的进行拼接。
从代码量和实现上来看,这种方案会更轻量、更巧妙,而且不需要通过eval等方法动态生成或执行函数。
4. 总结
虽然不同库的实现不同,但从整体思路来看,实现高性能的方式是相同的:
归根结底,它本质上是通过静态结构信息来预设优化和分析。
尖端
最后我还想提一下