详解如何提升JSON.stringify()的性能


Posted in Javascript onJune 12, 2019

1. 熟悉的JSON.stringify()

在浏览器端或服务端,JSON.stringify()都是我们很常用的方法:

  • 将 JSON object 存储到 localStorage 中;
  • POST 请求中的 JSON body;
  • 处理响应体中的 JSON 形式的数据;
  • 甚至某些条件下,我们还会用它来实现一个简单的深拷贝;
  • ……

在一些性能敏感的场合下(例如服务端处理大量并发),或面对大量 stringify 的操作时,我们会希望它的性能更好,速度更快。这也催生了一些优化的 stringify 方案/库,下图是它们与原生方法的性能对比:

详解如何提升JSON.stringify()的性能

绿色部分时原生JSON.stringify(),可见性能相较这些库都要低很多。那么,在大幅的性能提升背后的技术原理是什么呢?

2. 比 stringify 更快的 stringify

由于 JavaScript 是动态性很强的语言,所以对于一个 Object 类型的变量,其包含的键名、键值、键值类型最终只能在运行时确定。因此,执行JSON.stringify()时会有很多工作要做。在一无所知的情况下,我们想要大幅优化显然无能为力。

那么如果我们知道这个 Object 中的键名、键值信息呢 —— 也就是知道它的结构信息,这会有帮助么?

看个例子:

下面这个 Object,

const obj = { 
 name: 'alienzhou', 
 status: 6, 
 working: true 
};

我们对它应用JSON.stringify(),得到结果为

JSON.stringify(obj); 
// {"name":"alienzhou","status":6,"working":true}

现在如果我们知道这个obj的结构是固定的:

  • 键名不变
  • 键值的类型一定

那么其实,我可以创建一个“定制化”的 stringify 方法

function myStringify(o) { 
 return ( 
  '{"name":"' 
  + o.name 
  + '","status":' 
  + o.status 
  + ',"isWorking":' 
  + o.working 
  + '}' 
 ); 
}

看看我们的myStringify方法的输出:

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 对象即可。

这也是大多数 stringify 加速库的套路,转化为代码就是类似:

import faster from 'some_library_faster_stringify'; 
// 1. 通过相应规则,定义你的对象结构 
const theObjectScheme = { 
 // …… 
}; 
// 2. 根据结构,得到一个定制化的方法 
const stringify = faster(theObjectScheme); 
// 3. 调用方法,快速 stringify 
const target = { 
 // …… 
}; 
stringify(target);

3. 如何生成“定制化”的方法

根据上面的分析,核心功能在于,根据其结构信息,为该类对象创建“定制化”的stringify方法,其内部实际是简单的属性访问与字符串拼接。

为了了解具体的实现方式,下面我以两个实现上略有差异的开源库为例来简单介绍一下。

3.1. fast-json-stringify

详解如何提升JSON.stringify()的性能

下图是根据 fast-json-stringify 提供的 benchmark 结果,整理出来的性能对比。

详解如何提升JSON.stringify()的性能

可以看到,在大多数场景下具备2-5倍的性能提升。

3.1.1. scheme 的定义方式

fast-json-stringify 使用了 JSON Schema Validation 来定义(JSON)对象的数据格式。其 scheme 定义的结构本身也是 JSON 格式的,例如对象

{ 
 name: 'alienzhou', 
 status: 6, 
 working: true 
}

对应的 scheme 就是:

{ 
 title: 'Example Schema', 
 type: 'object', 
 properties: { 
  name: { 
   type: 'string' 
  }, 
  status: { 
   type: 'integer' 
  }, 
  working: { 
   type: 'boolean' 
  } 
 } 
}

其 scheme 定义规则丰富,具体使用可以参考 Ajv 这个 JSON 校验库。

3.1.2. stringify 方法的生成

fast-json-stringify 会根据刚才定义的 scheme,拼接生成出实际的函数代码字符串,然后使用 Function 构造函数在运行时动态生成对应的 stringify 函数。

在代码生成上,首先它会注入预先定义好的各类工具方法,这一部分不同的 scheme 都是一样的:

var code = ` 
 'use strict' 
 ` 
 code += ` 
 ${$asString.toString()} 
 ${$asStringNullable.toString()} 
 ${$asStringSmall.toString()} 
 ${$asNumber.toString()} 
 ${$asNumberNullable.toString()} 
 ${$asIntegerNullable.toString()} 
 ${$asNull.toString()} 
 ${$asBoolean.toString()} 
 ${$asBooleanNullable.toString()} 
 `

其次,就会根据 scheme 定义的具体内容生成 stringify 函数的具体代码。而生成的方式也比较简单:通过遍历 scheme。

遍历 scheme 时,根据定义的类型,在对应代码处插入相应的工具函数用于键值转换。例如上面例子中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变量保存的就是最后生成的函数体的代码串。由于在 scheme 定义中,name为string类型,且不为空,所以会在code中添加如下一段代码字符串:

"json += $asString(obj['name'])"

由于还需要处理数组、及联对象等复杂情况,实际的代码省略了很多。

然后,生成的完整的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字符串传入 Function 构造函数来创建相应的 stringify 函数。

// dependencies 主要用于处理包含 anyOf 与 if 语法的情况 
dependenciesName.push(code) 
return (Function.apply(null, dependenciesName).apply(null, dependencies))

3.2. slow-json-stringify

详解如何提升JSON.stringify()的性能

slow-json-stringify 虽然名字叫 "slow",但其实是一个 "fast" 的 stringify 库(命名很调皮)。

The slowest stringifier in the known universe. Just kidding, it's the fastest (:

它的实现比前面提到的 fast-json-stringify 更轻量级,思路也很巧妙。同时它在很多场景下效率会比 fast-json-stringify 更快。

详解如何提升JSON.stringify()的性能

详解如何提升JSON.stringify()的性能

3.2.1. scheme 的定义方式

slow-json-stringify 的 scheme 定义更自然与简单,主要就是将键值替换为类型描述。还是上面这个对象的例子,scheme 会变为

{ 
 name: 'string', 
 status: 'number', 
 working: 'boolean' 
}

确实非常直观。

3.2.2. stringify 方法的生成

不知道你注意到没有

// scheme 
{ 
 name: 'string', 
 status: 'number', 
 working: 'boolean' 
} 
// 目标对象 
{ 
 name: 'alienzhou', 
 status: 6, 
 working: true 
}

scheme 和原对象的结构是不是很像?

这种 scheme 的巧妙之处在于,这样定义之后,我们可以先把 scheme JSON.stringify一下,然后“扣去”所有类型值,最后等着我们的就是把实际的值直接填充到 scheme 对应的类型声明处。

具体如何操作呢?

首先,可以直接对 scheme 调用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__",并基于"__par__"拆分字符串:

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__');

这样你就会得到chunks和props两个数组。chunks里包含了被分割的 JSON 字符串。以例子来说,两个数组分别如下

// chunks 
[ 
 '{"name":"', 
 '","status":"', 
 '","working":"', 
 '"}' 
] 
// props 
[ 
 'name', 
 'status', 
 'working' 
]

最后,由于 map 中保存了属性名与访问路径的映射,因此可以根据 prop 访问到对象中某个属性的值,循环遍历数组,将其与对应的 chunks 拼接即可。

从代码量和实现方式来看,这个方案会更轻便与巧妙,同时也不需要通过 Function、eval 等方式动态生成或执行函数。

4. 总结

虽然不同库的实现有差异,但从整体思路上来说,实现高性能 stringify 的方式都是一样的:

  • 开发者定义 Object 的 JSON scheme;
  • stringify 库根据 scheme 生成对应的模版方法,模版方法里会对属性与值进行字符串拼接(显然,属性访问与字符串拼接的效率要高多了);
  • 最后开发者调用返回的方法来 stringify Object 即可。

归根到底,它本质上是通过静态的结构信息将优化与分析前置了。

Tips

最后,还是想提一下

所有的 benchmark 只能作为一个参考,具体是否有性能提升、提升多少还是建议你在实际的业务中测试;

fast-json-stringify 中使用到了 Function 构造函数,因此建议不要将用户输入直接用作 scheme,以防一些安全问题。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript结合Canvas 实现简易的圆形时钟
Mar 11 Javascript
JS时间特效最常用的三款
Aug 19 Javascript
DeviceOne 让你一见钟情的App快速开发平台
Feb 17 Javascript
浅析JS动态创建元素【两种方法】
Apr 20 Javascript
解析Javascript单例模式概念与实例
Dec 05 Javascript
JS获取多维数组中相同键的值实现方法示例
Jan 06 Javascript
AngularJS 支付倒计时功能实现思路
Jun 05 Javascript
jQuery实现鼠标点击处心形漂浮的炫酷效果示例
Apr 12 jQuery
layer实现弹出层自动调节位置
Sep 05 Javascript
js实现随机div颜色位置 类似满天星效果
Oct 24 Javascript
浅谈javascript如何获取文件后缀名
Aug 07 Javascript
javascript前端和后台进行数据交互方法示例
Aug 07 Javascript
基于vue-cli搭建多模块且各模块独立打包的项目
Jun 12 #Javascript
浅谈webpack 四个核心概念之Entry
Jun 12 #Javascript
vue安装遇到的5个报错及解决方法
Jun 12 #Javascript
深入学习JavaScript 高阶函数
Jun 11 #Javascript
javascript防抖函数debounce详解
Jun 11 #Javascript
详解ng-alain动态表单SF表单项设置必填和正则校验
Jun 11 #Javascript
vue实现路由懒加载及组件懒加载的方式
Jun 11 #Javascript
You might like
删除html标签得到纯文本可处理嵌套的标签
2014/04/28 PHP
详解PHP编码转换函数应用技巧
2016/10/22 PHP
jquery.Jwin.js 基于jquery的弹出层插件代码
2012/05/23 Javascript
表单的焦点顺序tabindex和对应enter键提交
2013/01/04 Javascript
JavaScript控制各种浏览器全屏模式的方法、属性和事件介绍
2014/04/03 Javascript
聊一聊JS中this的指向问题
2016/06/17 Javascript
JavaScript检测原始值、引用值、属性
2016/06/20 Javascript
jQuery实现左侧导航模块的显示与隐藏效果
2016/07/04 Javascript
微信小程序 开发工具快捷键整理
2016/10/31 Javascript
js 模仿锚点定位的实现方法
2016/11/19 Javascript
Bootstrap整体框架之JavaScript插件架构
2016/12/15 Javascript
jQuery操作复选框(CheckBox)的取值赋值实现代码
2017/01/10 Javascript
微信小程序 网络请求(post请求,get请求)
2017/01/17 Javascript
Angularjs的键盘事件的绑定
2017/07/27 Javascript
在vue项目中引入highcharts图表的方法(详解)
2018/03/05 Javascript
JS加密插件CryptoJS实现AES加密操作示例
2018/08/16 Javascript
vue移动端微信授权登录插件封装的实例
2018/08/28 Javascript
关于JavaScript 数组你应该知道的事情(推荐)
2019/04/10 Javascript
基于Vue 撸一个指令实现拖拽功能
2019/10/09 Javascript
Vue项目如何引入bootstrap、elementUI、echarts
2020/11/26 Vue.js
Python写的Tkinter程序屏幕居中方法
2015/03/10 Python
Python闭包实现计数器的方法
2015/05/05 Python
对python中return和print的一些理解
2017/08/18 Python
在python3中pyqt5和mayavi不兼容问题的解决方法
2019/01/08 Python
对Pytorch中nn.ModuleList 和 nn.Sequential详解
2019/08/18 Python
python 通过手机号识别出对应的微信性别(实例代码)
2019/12/22 Python
浅谈matplotlib 绘制梯度下降求解过程
2020/07/12 Python
Python实现异步IO的示例
2020/11/05 Python
python代码实现猜拳小游戏
2020/11/30 Python
Swisse官方海外旗舰店:澳大利亚销量领先,自然健康品牌
2017/12/15 全球购物
汽车装潢店创业计划书范文
2014/02/05 职场文书
《得道多助,失道寡助》教学反思
2014/04/19 职场文书
绵山导游词
2015/02/05 职场文书
React中的Context应用场景分析
2021/06/11 Javascript
CSS+HTML 实现顶部导航栏功能
2021/08/30 HTML / CSS
mysql事务隔离级别详情
2021/10/24 MySQL