最近對slow-json-stringify的源碼研究了一下,本文將對源碼中函數的做用進行講解。下文的源碼是2.0.0版本的代碼。git
JSON.stringify能夠把一個對象轉化爲一個JSON字符串。slow-json-stringify
開源庫是對上述功能進行了一個時間上的優化。github
由於JavaScript是動態類型的語言,因此一個對象的屬性值的類型在運行的時候才能肯定,所以執行JSON.stringify
會有不少和肯定變量類型相關的工做。正則表達式
那麼,若是咱們事先能夠知道JSON的格式,是否是就能夠縮減一些時間?slow-json-stringify
正是基於這個思路去作的。因此,你須要提供一個說明屬性值類型的schema
,它會根據schema
生成一個單獨的stringify
方法。json
基本原理就是根據提供的schema
,把字符串分割成兩部分,chunks
和queue
:數組
chunks
裏面用於存放字符串中不變的部分queue
存放生成動態屬性值相關的信息當序列化實際對象的時候,再把這兩部分拼接起來。ide
// 咱們須要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
實際以下:
spa
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
。接下來咱們一一介紹。
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
是把不變的部分按照順序提取成一個數組。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
。
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
方法在起做用。
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
方法下面的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'])
...,直到堆棧溢出。
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
的時候,該靜態字符串前面用於拼合前面屬性的部分。
因此返回值裏面的這幾個屬性分別表示:
undefined
undefined
的時候用這個原始靜態字符串,咱們簡版裏面就是使用的這個字段undefined
的時候,用這個處理事後的靜態字符串undefined
的時候,用這個處理事後的靜態字符串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 };
undefined
,因此使用了prevUndef
undefined
,因此使用了prue
undefined
,因此使用了bothUndef
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":[]}"
本文到這裏就結束了,與君共勉。