共计 12076 个字符,预计需要花费 31 分钟才能阅读完成。
最近对 slow-json-stringify 的源码钻研了一下,本文将对源码中函数的作用进行解说。下文的源码是 2.0.0 版本的代码。
简介
JSON.stringify 能够把一个对象转化为一个 JSON 字符串。slow-json-stringify
开源库是对上述性能进行了一个 工夫 上的优化。
因为 JavaScript 是动静类型的语言,所以一个对象的属性值的类型在运行的时候能力确定,因而执行 JSON.stringify
会有很多和确定变量类型相干的工作。
那么,如果咱们当时能够晓得 JSON 的格局,是不是就能够缩减一些工夫?slow-json-stringify
正是基于这个思路去做的。所以,你须要提供一个阐明属性值类型的 schema
,它会依据schema
生成一个独自的 stringify
办法。
基本原理就是依据提供的 schema
,把字符串宰割成两局部,chunks
和queue
:
chunks
外面用于寄存字符串中 不变 的局部queue
寄存生成动静属性值相干的信息
当序列化理论对象的时候,再把这两局部拼接起来。
应用
schema 定义
// 咱们须要 stringify 的对象
var obj = {
a: 'world', // 字符串类型
b: 42, // 数字类型
c: true, // 布尔类型
d: [ // 数组中每一项都是同样的构造
{
e: 'value1',
f: 3
},
{
e: 'value2',
f: 4
}
]
}
var schema = {a: attr('string'), // 不是 'string',应用了它提供的 attr 办法
b: attr('number'), // 不是 'number',应用了它提供的 attr 办法
c: attr('boolean'), // 不是 'boolean',应用了它提供的 attr 办法
d: attr('array', sjs({e: attr('string'),
f: attr('number')
}))
}
var stringify = sjs(schema) // sjs 函数针对每一个 schema 生成一个独自的 stringify 办法
stringify(obj) // "{"a":"world","b":42,"c":true,"d":[{"e":"value1","f":3},{"e":"value2","f":4}]}"
简化版本代码剖析
刚开始剖析的时候,咱们能够大抵理解下每个函数的性能,不必太思考各种细节,等咱们把整体流程理解实现之后,再看细节局部。
咱们以上面这个最简略的 schema
为例进行解说:
var schema = {a: attr('string'),
b: attr('number'),
c: attr('boolean')
}
在下面应用的时候,咱们发现次要用了两个函数,attr
和 sjs
(slow json stringify 的缩写),咱们先看下attr
函数完整版:
const attr = (type, serializer) => {if (!TYPES.includes(type)) { // 容错解决,能够先不思考
throw new Error(`Expected one of: "number", "string", "boolean", "null". received "${type}" instead`);
}
const usedSerializer = serializer || (value => value); // 自定义每个属性的 stringify 办法,能够先不思考
return {
isSJS: true,
type,
serializer: type === 'array'
? _makeArraySerializer(serializer) // 数组类型,做非凡解决,能够先不思考
: usedSerializer,
};
};
简化后的版本如下:
const attr = (type, serializer) => {
const usedSerializer = value => value;
return {
isSJS: true,
type,
serializer: usedSerializer,
};
};
能够看到 attr
承受两个参数:类型和自定义序列化函数,上述 schema
理论如下:
sjs
sjs
函数完整版代码如下:
const sjs = (schema) => {const { preparedString, preparedSchema} = _prepare(schema);
const queue = _makeQueue(preparedSchema, schema);
const chunks = _makeChunks(preparedString, queue);
const selectChunk = _select(chunks);
...
};
sjs 函数用了多个办法,_prepare
, _makeQueue
, _makeChunks
, _select
。接下来咱们一一介绍。
_prepare
const _prepare = (schema) => {const preparedString = JSON.stringify(schema, (_, value) => {if (!value.isSJS) return value;
return `${value.type}__sjs`;
});
const preparedSchema = JSON.parse(preparedString);
return {
preparedString,
preparedSchema, // preparedString 对应的 json 对象
};
};
_prepare(schema)
// preparedString: "{"a":"string__sjs","b":"number__sjs","c":"boolean__sjs"}"
// preparedSchema: {a:"string__sjs",b:"number__sjs",c:"boolean__sjs"}
// schema: {a:attr('string'),b:attr('number'),c:attr('boolean')}
比照下会发现 _prepare
把attr('type')
模式转化成了 type_sjs
模式。为什么这么转呢?咱们发现只有把 preparedString
外面的 type_sjs
替换成真正的值就能够了。所以,咱们能够把 prepareString
外面不变的局部和变的局部离开,而后依照程序再把他们拼接起来:不变的局部 + 变的局部 + 不变的局部 + 变的局部 +…+ 不变的局部。所以就有了上面这两个办法:
_makeQueue
是把 变的局部依照程序 提取成一个数组。_makeChunks
是把 不变的局部依照程序 提取成一个数组。
_makeQueue
const _makeQueue = (preparedSchema, originalSchema) => {const queue = [];
(function scoped(obj, acc = []) {
// 后面_prepare 生成的 preparedSchema 把属性值变成了 type__sjs 的模式,所以如果属性值蕴含__sjs,咱们能够认为这就是变量局部
if (/__sjs/.test(obj)) {const usedAcc = Array.from(acc);
const find = _find(usedAcc); // 从理论对象中获取这个变量值的办法,usedAcc 是这个属性数组模式的拜访门路
const {serializer} = find(originalSchema); // 从原始 schema 获取序列化办法
queue.push({
serializer, // 该属性值序列化的办法
find, // 从对象中获取属性值的办法
name: acc[acc.length - 1], // 属性名
});
return;
}
return Object
.keys(obj)
.map(prop => scoped(obj[prop], [...acc, prop]));
})(preparedSchema);
return queue;
};
_makeQueue(_prepare(schema).preparedSchema, schema)
能够看到 find
办法是咱们获取理论属性值的办法。咱们看下 _find
函数:
const _find = (path) => {const { length} = path;
let str = 'obj';
for (let i = 0; i < length; i++) {
// 简略的容错
str = str.replace(/^/, '(');
str += ` || {}).${path[i]}`;
// 如果不做容错解决,能够间接用上面的
// str += `.${path[i]}`
}
return eval(`((obj) => ${str})`);
};
path
是对象某个属性的拜访门路上的所有属性名组成的数组,比方对象:
var hello = {
a: {
b: {c: 'world'}
}
}
属性值 'world'
的拜访门路就是 ['a', 'b', 'c']
,咱们把这个path
传给 _find
,就会给咱们返回一个应用eval
动静生成的函数(obj) => (((obj.a || {}).b || {}).c
。
如果应用下面我说的不做容错解决的版本,那么返回的函数就是(obj) => obj.a.b.c
。
_makeChunks
const _makeChunks = (str, queue) => str
.replace(/"\w+__sjs"/gm, type => (/string/.test(type) ? '"__par__"' : '__par__'))
.split('__par__')
.map((chunk, index, chunks) => {const matchProp = `("${(queue[index] || {}).name}":(\"?))$`;
const matchWhenLast = `(\,?)${matchProp}`;
const isLast = /^("}|})/.test(chunks[index + 1] ||'');
const matchPropRe = new RegExp(isLast ? matchWhenLast : matchProp);
const matchStartRe = /^(\"\,|\,|\")/;
return {
flag: false, // 表明后面的属性值是不是 undefined
pure: chunk,
prevUndef: chunk.replace(matchStartRe, ''),
isUndef: chunk.replace(matchPropRe, ''),
bothUndef: chunk
.replace(matchStartRe, '')
.replace(matchPropRe, ''),
};
});
下面的咋一看挺简单,好多正则正则表达式,他们是用来解决属性值是 undefined
的状况。
JSON.stringify
转换成 json 字符串的过程中,如果这个属性值是undefined
,这个属性不会呈现在最终的字符串中,如下:
JSON.stringify({a: 'hello', b: undefined}) // "{"a":"hello"}"
咱们能够先不思考属性值是 undefined
的状况,那么,_makeQueue
能够简化如下:
// str 是后面通过_prepare 生成的 preparedString
const _makeChunks = (str) => str
.replace(/"\w+__sjs"/gm, type => (/string/.test(type) ? '"__par__"' : '__par__'))
.split('__par__')
.map((chunk, index, chunks) => {
return {
flag: false, // 表明后面的属性值是不是 undefined
pure: chunk
};
});
var preparedString = _prepare(schema).preparedString
// "{"a":"string__sjs","b":"number__sjs","c":"boolean__sjs"}"
_makeChunks(preparedString)
通过后果会发现 _makeChunks
就是把 不变 的局部依照程序提取成一个数组。咱们晓得字符串 stringify
之后,属性值是被双引号突围的,数字或者布尔值 stringify
之后,属性值是不被双引号突围的,所以 string__sjs
两边的双引号是须要保留放在 chunk
外面的,数字和布尔类型是须要去掉双引号的。这就是下面 replace
办法的作用。而后再以 __par__
宰割字符串。
察看下面截图中 pure
属性,会发现 a
属性值那比 b
和c
多一个双引号。这个就是 replace
办法在起作用。
select 办法
const _select = chunks => (value, index) => {const chunk = chunks[index];
if (typeof value !== 'undefined') {if (chunk.flag) {return chunk.prevUndef + value;}
return chunk.pure + value;
}
chunks[index + 1].flag = true;
if (chunk.flag) {return chunk.bothUndef;}
return chunk.isUndef;
};
后面咱们说了不思考属性值是 undefined
的状况,所以第一个 if
判断就是 true
,就不必思考上面的状况了。而chunk
的flag
是表明后面的属性值是不是 undefined
的,在不思考属性值是 undefined
的状况下,这个 flag
永远是 false
。这两步精简后的_select
函数如下:
const _select = chunks => (value, index) => {const chunk = chunks[index];
return chunk.pure + value;
};
chunk.pure
就是后面 _makeChunks
生成的,应用 __par__
宰割生成的字符串。
_select
办法用来拼接不变的局部 chunk
和通过 queue
失去的理论属性值。
上面咱们接着讲 sjs
函数:
const sjs = (schema) => {const { preparedString, preparedSchema} = _prepare(schema);
const queue = _makeQueue(preparedSchema, schema);
const chunks = _makeChunks(preparedString, queue);
const selectChunk = _select(chunks);
const {length} = queue;
return (obj) => {
let temp = '';
let i = 0;
while (true) {if (i === length) break;
const {serializer, find} = queue[i];
const raw = find(obj); // 找到这个属性的理论属性值
temp += selectChunk(serializer(raw), i);
i += 1; // 解决下一个属性值
}
const {flag, pure, prevUndef} = chunks[chunks.length - 1]; // 拼接最初一个不变的局部
return temp + (flag ? prevUndef : pure);
};
};
sjs 函数返回了一个函数,这个函数的参数是咱们将要 stringify
的 json 对象。这个函数会通过循环的形式遍历 queue
数组,queue
数组存储的就是变量的局部。通过 find
办法找到变量的原始值,而后通过 serializer
办法返回自定义的值,通过 selectChunk
办法返回该属性值后面不变的局部 + 属性值。
最初在加上最初一个不变的局部,这个过程就实现了。咱们会发现 queue
的长度始终比 chunks
的长度小一。
残缺版本代码剖析
咱们通过几个例子来对应看下在简化版本咱们疏忽的局部
例子一:嵌套对象
var schema = {a: attr('string'),
b: attr('number'),
c: {d: attr('string'),
e: attr('number')
}
}
_makeQueue
咱们来剖析下 _makeQueue
办法上面的Object.keys()
:
const _makeQueue = (preparedSchema, originalSchema) => {const queue = [];
(function scoped(obj, acc = []) {if (/__sjs/.test(obj)) {// ...}
return Object
.keys(obj)
.map(prop => scoped(obj[prop], [...acc, prop]));
})(preparedSchema);
return queue;
};
scoped
函数开始执行的时候,首先是一个 if 判断,刚开始 obj
就是preparedSchema
,是一个对象,那么正则表达式的 test 函数承受一个对象做为参数做了什么呢?
因为 test 函数的含意就是测试一个字符串是否满足正则表达式,当遇到非字符串参数的时候,会首先把参数转化为字符串,所以给 test 传入 preparedSchema 的时候,首先调用了对象的 toString
办法,一般对象调用 toString
办法个别返回[object Object]
:
/__sjs/.test({name: 'hello'}) // false
/\[object Object\]/.test({name: 'hello'}) // true
scoped 函数刚开始执行的时候 if 判断失败,会走到 Object.keys
。同理,如果遇到嵌套的对象,就像下面这个例子,当剖析到属性c
的值的时候,也会应用 Object.keys
遍历外面的 d
和e
属性,同时 acc
变量会把以后拜访门路加进去。
不过,如果定义的时候没有应用 attr 属性,就有可能会导致堆栈溢出:
// 没有依照标准定义 schema
var schema = {
a: 'string',
b: attr('string')
}
在_prepare 办法外面 JSON.stringify 的时候,咱们间接应用的 'string'
在!value.isSJS
这个判断中胜利,所以间接返回了value
:
const _prepare = (schema) => {const preparedString = JSON.stringify(schema, (_, value) => {if (!value.isSJS) return value;
// ...
});
// ...
};
所以_prepare 返回值外面的 preparedSchema 如下:
{a: "string", b: "string__sjs"}
接下来执行 _makeQueue
的时候,当实现 preparedSchema 解决之后,会开始解决属性 a 的属性值,也就是scoped('string', ['a'])
,首先 if 判断是失败的,执行Object.keys('string')
:
Object.keys('string') // ["0", "1", "2", "3", "4", "5"]
Object.keys('hello') // ["0", "1", "2", "3", "4"]
Object.keys(123) // []
Object.keys(true) // []
这里暗藏着另外一个知识点:Object.keys
会首先把参数转换成对象,也就是new String('string')
咱们接着往下看,
Object.keys('string').map(prop => scoped(obj[prop], [...acc, prop])
map 的时候 prop 就是 0,1,2,3,4,5,obj[prop]就是每个字符,所以会进入scoped('s', ['a', '0']), scoped('t', ['a', '1']) ...
。
看第一个 scoped('s', ['a', '0'])
,就会进入和下面一样的剖析过程,只不过原先的参数'string'
变成了's'
,所以会进入到scoped('s', ['a', '0', '0'])
,而后再进入到scoped('s', ['a', '0', '0', '0'])
…,直到堆栈溢出。
例子二:属性值 undefined
var schema = {a: attr('string'),
b: attr('number'),
c: attr('string')
}
// 须要 stringify 的对象
var obj = {
a: undefined,
b: undefined,
c: undefined
}
咱们后面讲过_makeChunks 是用来提取 stringify 后的字符串外面不变的局部的,简化版本删除了和属性值是 undefined
相干的代码,咱们当初来看下:
const _makeChunks = (str, queue) => str
.replace(/"\w+__sjs"/gm, type => (/string/.test(type) ? '"__par__"' : '__par__'))
.split('__par__')
.map((chunk, index, chunks) => {const matchProp = `("${(queue[index] || {}).name}":(\"?))$`;
const matchWhenLast = `(\,?)${matchProp}`;
const isLast = /^("}|})/.test(chunks[index + 1] ||'');
const matchPropRe = new RegExp(isLast ? matchWhenLast : matchProp);
const matchStartRe = /^(\"\,|\,|\")/;
return {
flag: false, // 表明后面的属性值是不是 undefined
pure: chunk,
// Without initial part
prevUndef: chunk.replace(matchStartRe, ''),
// Without property chars
isUndef: chunk.replace(matchPropRe, ''),
// Only remaining chars (can be zero chars)
bothUndef: chunk
.replace(matchStartRe, '')
.replace(matchPropRe, ''),
};
});
queue
参数就是后面 _makeQueue
办法生成的用于寄存变的局部的相干信息。当属性值是 undefined
的时候,属性名也不会呈现在最终的字符串中。然而咱们生成的 chunks
是蕴含属性名的,所以须要用正则把属性名给删掉。
matchProp
匹配的属性的键值局部,也就是 "key":"
或者 "key":
,前面这个引号是字符串类型的时候会有,其余类型的时候没有,和后面的replace
办法对应。
matchWhenLast
匹配当 undefined
的属性是这个对象最初一个属性的时候,这个属性后面的逗号也要去掉。
isLast
是用于判断这个属性是不是对象的最初一个属性的,依据这个判断是用 mathProp
还是matchWhenLast
。
matchPropRe
就是依据后面 isLast
判断之后的最终的正则表达式。
matchStartRe
是,当后面一个属性是 undefined
的时候,该动态字符串后面用于拼合后面属性的局部。
所以返回值外面的这几个属性别离示意:
- flag: 后面的属性值是不是
undefined
- pure: 后面的属性值和该动态字符串前面的属性值都不是
undefined
的时候用这个原始动态字符串,咱们简版外面就是应用的这个字段 - prevUndef: 只有后面的属性值是
undefined
的时候,用这个解决过后的动态字符串 - isUndef: 只有该动态字符串前面的属性值是
undefined
的时候,用这个解决过后的动态字符串 - bothUndef: 后面的属性值和该动态字符串前面的属性值都是
undefined
的时候,应用这个解决后的动态字符串
接下来剖析 _select
办法,这几个字段是在 _select
中被生产的:
const _select = chunks => (value, index) => {const chunk = chunks[index];
if (typeof value !== 'undefined') {if (chunk.flag) {return chunk.prevUndef + value; // 11}
return chunk.pure + value; // 12
}
chunks[index + 1].flag = true; // 标记前面动态字符串后面的属性值是 undefined
if (chunk.flag) {return chunk.bothUndef; // 21}
return chunk.isUndef; // 22
};
- 11 对应只有后面的属性值是
undefined
,所以应用了prevUndef
- 12 对应前后属性值都不是
undefined
,所以应用了prue
- 21 对应前后属性值都是
undefined
,所以应用了bothUndef
- 22 对应只有前面的属性值是
undefined
,所以应用了isUndef
上面看下例子:
var schema = {a: attr('string'),
b: attr('number'),
c: attr('string')
}
obj = {
a: undefined,
b: undefined,
c: undefined
}
sjs(schema)(obj) // "{}"
从上图中看出其实后果就是chunks[0].isUndef + chunks[1].bothUndef + chunks[2].bothUndef + chunks[3].prevUndef
,所以最初的后果是"{}"
。
再看上面的例子
obj = {
a: undefined,
b: 3,
c: undefined
}
sjs(schema)(obj) // "{"b":3}"
从上图中看出其实后果就是chunks[0].isUndef + chunks[1].prevUndef + 3 + chunks[2].isUndef + chunks[3].prevUndef
,所以最初的后果是"{"b":3}"
。
例子二:数组
var schema = {
a: attr('array', sjs({b: attr('string'),
c: attr('number'),
}))
}
var obj = {
a: [
{
b: 'hello',
c: 1
},
{
b: 'hello',
c: 2
}
]
}
var stringify = sjs(schema)
stringify(obj) // "{"a":[{"b":"hello","c":1},{"b":"hello","c":2}]}"
咱们看下 attr
办法外面和数组类型相干的代码
const attr = (type, serializer) => {
// ...
return {
isSJS: true,
type,
serializer: type === 'array'
? _makeArraySerializer(serializer) // 数组类型,做非凡解决
: usedSerializer,
};
};
看下_makeArraySerializer 办法:
const _makeArraySerializer = (serializer) => {if (serializer instanceof Function) {return (array) => {
// Stringifying more complex array using the provided sjs schema
let acc = '';
const {length} = array;
for (let i = 0; i < length - 1; i++) {acc += `${serializer(array[i])},`;
}
// Prevent slice for removing unnecessary comma.
acc += serializer(array[length - 1]);
return `[${acc}]`;
};
}
return array => JSON.stringify(array);
};
从上述代码能够发现,如果没有定义能够的序列化办法,会间接调用 JSON.stringify
办法,也就是咱们的 schema
能够间接写成:
var schema = {a: attr('array')
}
sjs(schema)(obj) // {"a":[{"b":"hello","c":1},{"b":"hello","c":2}]}
然而这种 stringify 字符串的时候还是应用原生的 JSON.stringify 办法。
当定义了可用的数组序列化办法的时候,咱们会发现其实这个办法是用来 stringify 每一项的办法,所以数组的序列化办法要做的就是:
把数组的每一项应用序列化办法调用一下,而后把后果拼成数组的模式。须要拼凑的局部包含前后的 []
以及每项之间的宰割符,
。
这段代码遍历后面 length - 1
个元素,每个元素前面拼上逗号,最初再拼上最初一个数据项。然而,拼上最有一个数据项的时候没有做任何判断,如果数组长度是 0
也会拼上一项,所以导致最初的后果是多一个{}
:
var schema = {
a: attr('array', sjs({b: attr('string'),
c: attr('number'),
}))
}
var obj = {a: []
}
var stringify = sjs(schema)
stringify(obj) // "{"a":[{}]}"
var schema = {a: attr('array')
}
var stringify = sjs(schema)
stringify(obj) // "{"a":[]}"
JSON.stringify(obj) // "{"a":[]}"
结语
本文到这里就完结了,与君共勉。