前言
相信大家對JSON.stringify
並不陌生,通常在很多場景下都會用到這個API,最常見的就是HTTP請求中的資料傳輸, 因為HTTP 協議是一個文字協議,傳輸的格式都是字串,但我們在程式碼中常常操作的是 JSON 格式的資料,所以我們需要在返回響應資料前將 JSON 資料序列化為字串。但大家是否考慮過使用JSON.stringify
可能會帶來效能風險?,或者說有沒有一種更快的stringify
方法。
如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖
第一時間獲取最新文章~
JSON.stringify的效能瓶頸
由於 JavaScript 是動態語言,它的變數型別只有在執行時才能確定,所以 JSON.stringify 在執行過程中要進行大量的型別判斷,對不同型別的鍵值做不同的處理。由於不能做靜態分析,執行過程中的型別判斷這一步就不可避免,而且還需要一層一層的遞迴,迴圈引用的話還有爆棧的風險。
我們知道,JSON.string的底層有兩個非常重要的步驟:
- 型別判斷
- 遞迴遍歷
既然是這樣,我們可以先來對比一下JSON.stringify與普通遍歷的效能,看看型別判斷這一步到底是不是影響JSON.stringify效能的主要原因。
JSON.stringify 與遍歷對比
const obj1 = {}, obj2 = {}
for(let i = 0; i < 1000000; i++) {
obj1[i] = i
obj2[i] = i
}
function fn1 () {
console.time('jsonStringify')
const res = JSON.stringify(obj1) === JSON.stringify(obj2)
console.timeEnd('jsonStringify')
}
function fn2 () {
console.time("for");
const res = Object.keys(obj1).every((key) => {
if (obj2[key] || obj2[key] === 0) {
return true;
} else {
return false;
}
});
console.timeEnd("for");
}
fn1()
fn2()
從結果來看,兩者的效能差距在4倍左右,那就證明JSON.string
的型別判斷這一步還是非常耗效能的。如果JSON.stringify能夠跳過型別判斷這一步是否對型別判斷有幫助呢?
定製化更快的JSON.stringify
基於上面的猜想,我們可以來嘗試實現一下:
現在我們有下面這個物件
const obj = {
name: '南玖',
hobby: 'fe',
age: 18,
chinese: true
}
上面這個物件經過JSON.stringify
處理後是這樣的:
JSON.stringify(obj)
// {"name":"南玖","hobby":"fe","age":18,"chinese":true}
現在假如我們已經提前知道了這個物件的結構
- 鍵名不變
- 鍵值型別不變
這樣的話我們就可以定製一個更快的JSON.stringify方法
function myStringify(obj) {
return `{"name":"${obj.name}","hobby":"${obj.hobby}","age":${obj.age},"chinese":${obj.chinese}}`
}
console.log(myStringify(obj) === JSON.stringify(obj)) // true
這樣也能夠得到JSON.stringify一樣的效果,前提是你已經知道了這個物件的結構。
事實上,這是許多JSON.stringify
加速庫的通用手段:
-
需要先確定物件的結構資訊
-
再根據結構資訊,為該種結構的物件建立“定製化”的
stringify
方法 -
內部實現依然是這種字串拼接
更快的fast-json-stringify
fast-json-stringify 需要JSON Schema Draft 7輸入來生成快速
stringify
函式。
這也就是說fast-json-stringify
這個庫是用來給我們生成一個定製化的stringily函式,從而來提升stringify
的效能。
這個庫的GitHub簡介上寫著比 JSON.stringify() 快 2 倍,其實它的最佳化思路跟我們上面那種方法是一致的,也是一種定製化stringify
方法。
語法
const fastJson = require('fast-json-stringify')
const stringify = fastJson(mySchema, {
schema: { ... },
ajv: { ... },
rounding: 'ceil'
})
schema
: $ref 屬性引用的外部模式。ajv
: ajv v8 例項對那些需要ajv
.rounding
: 設定當integer
型別不是整數時如何舍入。largeArrayMechanism
:設定應該用於處理大型(預設情況下20000
或更多專案)陣列的機制
scheme
這其實就是我們上面所說的定製化物件結構,比如還是這個物件:
const obj = {
name: '南玖',
hobby: 'fe',
age: 18,
chinese: true
}
它的JSON scheme是這樣的:
{
type: "object",
properties: {
name: {type: "string"},
hobby: {type: "string"},
age: {type: "integer"},
chinese: {type: 'boolean'}
},
required: ["name", "hobby", "age", "chinese"]
}
AnyOf 和 OneOf
當然除了這種簡單的型別定義,JSON Schema 還支援一些條件運算,比如欄位型別可能是字串或者數字,可以用 oneOf 關鍵字:
"oneOf": [
{
"type": "string"
},
{
"type": "number"
}
]
fast-json-stringify
支援JSON 模式定義的anyOf和oneOf關鍵字。兩者都必須是一組有效的 JSON 模式。不同的模式將按照指定的順序進行測試。stringify
在找到匹配項之前必須嘗試的模式越多,速度就越慢。
anyOf和oneOf使用ajv作為 JSON 模式驗證器來查詢與資料匹配的模式。這對效能有影響——只有在萬不得已時才使用它。
關於 JSON Schema 的完整定義,可以參考 Ajv 的文件,Ajv 是一個流行的 JSON Schema驗證工具,效能表現也非常出眾。
當我們可以提前確定一個物件的結構時,可以將其定義為一個 Schema,這就相當於提前告訴 stringify 函式,需序列化的物件的資料結構,這樣它就可以不必再在執行時去做型別判斷,這就是這個庫提升效能的關鍵所在。
簡單使用
const fastJson = require('fast-json-stringify')
const stringify = fastJson({
title: 'myObj',
type: 'object',
properties: {
name: {
type: 'string'
},
hobby: {
type: 'string'
},
age: {
description: 'Age in years',
type: 'integer'
},
chinese: {
type: 'boolean'
}
}
})
console.log(stringify({
name: '南玖',
hobby: 'fe',
age: 18,
chinese: true
}))
//
生成 stringify 函式
fast-json-stringify
是跟我們傳入的scheme
來定製化生成一個stringily
函式,上面我們瞭解了怎麼為我們物件定義一個scheme
結構,接下來我們再來了解一下如何生成stringify
。
這裡有一些工具方法還是值得了解一下的:
const asFunctions = `
function $asAny (i) {
return JSON.stringify(i)
}
function $asNull () {
return 'null'
}
function $asInteger (i) {
if (isLong && isLong(i)) {
return i.toString()
} else if (typeof i === 'bigint') {
return i.toString()
} else if (Number.isInteger(i)) {
return $asNumber(i)
} else {
return $asNumber(parseInteger(i))
}
}
function $asNumber (i) {
const num = Number(i)
if (isNaN(num)) {
return 'null'
} else {
return '' + num
}
}
function $asBoolean (bool) {
return bool && 'true' || 'false'
}
// 省略了一些其他型別......
`
從上面我們可以看到,如果你使用的是 any 型別,它內部依然還是用的 JSON.stringify。 所以我們在用TS進行開發時應避免使用 any 型別,因為如果是基於 TS interface
生成 JSON Schema
的話,使用 any 也會影響到 JSON 序列化的效能。
然後就會根據 scheme 定義的具體內容生成 stringify 函式的具體程式碼。而生成的方式也比較簡單:透過遍歷 scheme,根據不同資料型別呼叫上面不同的工具函式來進行字串拼接。感興趣的同學可以在GitHub上檢視原始碼
總結
事實上fast-json-stringify
只是透過靜態的結構資訊將最佳化與分析前置了,透過開發者定義的scheme
內容可以提前知道物件的資料結構,然後會生成一個stringify
函式供開發者呼叫,該函式內部其實就是做了字串的拼接。
- 開發者定義 Object 的
JSON scheme
- stringify 庫根據 scheme 生成對應的模版方法,模版方法裡會對屬性與值進行字串拼接
- 最後開發者呼叫生成的stringify 方法
最後
我是南玖,我們下期見!!!