1。熟悉的JSON.stringify()在浏览器端或者服务器端,JSON.stringify()是我们很常用的一个方法:将JSON对象存储到localStorage中;POST请求中的JSON正文;处理body中的响应JSON格式数据;甚至在某些条件下,我们会用它来实现简单的深拷贝;...在一些对性能敏感的场合(比如服务器处理大量并发),或者面对大量的stringify,我们会期望它表现得更好,更快。这也导致了一些优化的stringify解决方案/库。下图是他们与原生方法的性能对比:绿色部分是原生的JSON.stringify(),可见性能比这些库要低很多。那么,性能大幅提升背后的技术原理是什么?2、Stringify比stringify更快由于JavaScript是一门非常动态的语言,对于一个Object类型的变量来说,它所包含的键名、键值、键值类型只能在运行时最终确定。因此,在执行JSON.stringify()时,需要做很多工作。在一无所知的情况下,我们无法做任何事情来彻底优化。那么如果我们知道了这个Object中的键名和键值信息——也就是知道了它的结构信息,这会有帮助吗?看一个例子:下面的Object,constobj={name:'alienzhou',status:6,working:true};我们对它应用JSON.stringify(),结果是JSON.stringify(obj);//{"name":"alienzhou","status":6,"working":true}现在如果我们知道这个obj的结构是固定的:键名不会改变键值的类型,那么实际上,我可以创建一个“自定义”stringify“方法functionmyStringify(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}可以得到正确的结果,只是用到了类型转换和字符串拼接,所以"custom""方法可以让"stringify"更快。综上所述,如何得到一个比stringify更快的stringify方法呢?需要先确定对象的结构信息;根据其结构信息,创建一个"自定义"的stringify此类结构对象的方法,实际上是通过字符串拼接产生结果;最后,使用这种“自定义”方法只是将对象字符串化。这也是大部分stringify加速库的套路,代码类似:importfasterfrom'some_library_faster_stringify';//1.通过相应的规则,定义你的对象结构consttheObjectScheme={//...};//2.根据结构,得到自定义的方法conststringify=faster(theObjectScheme);//3.调用方法,faststringifyconsttarget={//...};stringify(target);3、如何生成一个“定制化”的方法根据上面的分析,其核心功能是根据其结构信息为该类对象创建一个“定制化”的stringify方法,其内部其实就是简单的属性访问和字符串拼接.为了理解具体的实现方式,我简单介绍一下两个实现方式略有不同的开源库作为例子。3.1.fast-json-stringify下图是基于fast-json-stringify提供的benchmark结果的性能对比。可以看到,在大多数场景下,都有2-5倍的性能提升。3.1.1.如何定义方案fast-json-stringify使用JSONSchemaValidation来定义(JSON)对象的数据格式。它的scheme定义的结构也是JSON格式的。比如object{name:'alienzhou',status:6,working:true}对应的scheme是:{title:'ExampleSchema',type:'object',properties:{name:{type:'string'},status:{type:'integer'},working:{type:'boolean'}}}它的方案有丰富的定义规则。具体使用可以参考JSON验证库Ajv。3.1.2.stringify方法的生成fast-json-stringify会按照刚才定义的scheme拼接生成实际的函数代码字符串,然后在运行时使用Function构造函数动态生成对应的stringify函数。在代码生成方面,首先会注入各种预定义的工具方法,对于不同的方案都是一样的:varcode=`'usestrict'`code+=`${$asString.toString()}${$asStringNullable.toString()}${$asStringSmall.toString()}${$asNumber.toString()}${$asNumberNullable.toString()}${$asIntegerNullable.toString()}${$asNull.toString()}${$asBoolean.toString()}${$asBooleanNullable.toString()}`其次,stringify函数的具体代码会根据scheme定义的具体内容生成。而且生成的方式也比较简单:遍历scheme。遍历scheme时,根据定义的类型,在相应代码处插入相应的工具函数,进行key-value转换。比如上例中的属性名:varaccessor=key.indexOf('[')===0?sanitizeKey(key):`['${sanitizeKey(key)}']`switch(type){case'null':code+=`json+=$asNull()`breakcase'string':code+=nullable?`json+=obj${accessor}===null?null:$asString(obj${accessor})`:`json+=$asString(obj${accessor})`breakcase'integer':code+=nullable?`json+=obj${accessor}===null?null:$asInteger(obj${accessor}})`:`json+=$asInteger(obj${accessor})`break...上面代码中的code变量保存了最后生成的函数体的代码串。由于scheme定义中name是string类型且不为空,所以在代码中会加入如下代码字符串:"json+=$asString(obj['name'])"因为需要处理数组,以及对于链接对象等复杂情况,实际代码省略了很多。那么,生成的完整代码串大致如下:function$asString(str){//...}function$asStringNullable(str){//...}function$asStringSmall(str){//...}function$asNumber(i){//...}function$asNumberNullable(i){//...}/*以上是一系列通用的key-value转换方法*//*$main为主stringify的函数*/function$main(input){varobj=typeofinput.toJSON==='function'?input.toJSON():inputvarjson='{'varaddComma=falseif(obj['name']!==undefined){if(addComma){json+=','}addComma=truejson+='"name":'json+=$asString(obj['name'])}//...其他属性的拼接(status,working)json+='}'returnjson}return$main最后通过代码string到Function构造函数中以创建相应的stringify函数。//dependencies主要用于处理涉及anyOf和if的情况语法dependenciesName.push(code)return(Function.apply(null,dependenciesName).apply(null,dependencies))3.2.slow-json-stringifyslow-json-stringify虽然叫“慢”,但实际上是一个“快”的stringify库(名字有点调皮)。已知宇宙中最慢的字符串化器。开个玩笑,它是最快的(:它的实现比前面提到的fast-json-stringify更轻巧,思路也很巧妙。同时在很多场景下效率比fast-json-stringify更快。3.2.1.如何定义schemeslow-json-stringify的scheme定义更自然简单,主要是将key值替换成类型描述。还是上面对象的例子,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()的第二个参数作为遍历方法采集到的属性的访问路径:letmap={};conststr=JSON.stringify(schema,(prop,value)=>{constisArray=Array.isArray(value);if(typeofvalue!=='object'||isArray){if(isArray){constcurrent=value[0];}arrais.set(prop,current);}_validator(value);map[prop]=_deepPath(schema,prop);props+=`"${prop}"|`;}返回值;});至此,map中收集了所有属性的访问路径。同时可以将生成的props拼接成匹配对应类型字符的正则表达式。例如,我们示例中的正则表达式是/name|status|working"(string|number|boolean|undef)"|\\[(.*?)\\]/。然后,按照正则表达式依次匹配这些属性,将属性类型的字符串替换为统一的占位符字符串“__par__”,并根据“__par__”拆分字符串:constqueue=[];constchunks=str.replace(regex,(type)=>{switch(type){case'"string"':case'"undefined"':return'"__par__"';case'"number"':case'"boolean"':case'["array-simple"]':case'[null]':return'__par__';default:constprop=type.match(/(?<=\").+?(?=\")/)[0];queue.push(prop);返回类型;}}).split('__par__');这样,你将得到两个chunk和props数组。块包含拆分的JSON字符串。比如两个数组如下//chunks['{"name":"','","status":"','","working":"','"}']//props['name','status','working']最后,由于map中保存了属性名和访问路径的映射关系,所以可以根据prop访问对象中某个属性的值,以及可以循环遍历数组,并与对应的chunk进行拼接。从代码量和实现的角度来看,这种方案会更轻巧、更巧妙,不需要通过Function、eval等动态生成或执行函数。4.小结虽然不同库的实现不同,但是从整体思路来看,实现高性能stringify的方式是一样的:开发者定义Object的JSONscheme;stringify库根据scheme生成对应的模板方法,模板方法会对属性和值进行字符串拼接(显然,属性访问和字符串拼接的效率要高很多);最后,开发人员可以调用返回的方法对对象进行字符串化。归根结底,它本质上是通过静态结构信息进行前置优化和分析。Tips最后还是要提一下,所有的benchmarks都只能作为参考。是否有性能提升,提升多少,建议大家在实际业务中进行测试;fast-json-stringify中使用了Function构造函数,所以建议不要直接使用Userinput作为scheme,以防出现一些安全问题。
