[Роуминг Github] Как повысить производительность JSON.stringify()?

JSON оптимизация производительности
[Роуминг Github] Как повысить производительность JSON.stringify()?

Обычно я посещаю Github, и помимо некоторых крупных проектов с очень высокими звездами я также нахожу на Github много интересных небольших проектов. Проект или идея интересны или имеют хорошую техническую сторону, и их очень полезно читать. Поэтому я собираюсь написать серию «Бродячий Github», в которой буду делиться интересными проектами, с которыми время от времени сталкиваюсь на Github, и интерпретировать их.

1. знакомыйJSON.stringify()

На стороне браузера или сервера,JSON.stringify()Вот методы, которые мы используем очень часто:

  • Сохраните объект JSON в localStorage;
  • тело JSON в запросе POST;
  • Обрабатывать данные в формате JSON в теле ответа;
  • Даже при определенных условиях мы будем использовать его для реализации простой глубокой копии;
  • ...

В некоторых ситуациях, чувствительных к производительности (например, сервер обрабатывает много параллелизма) или при столкновении с большим количеством строковых операций, мы надеемся, что он будет работать лучше и быстрее. Это также породило некоторые оптимизированные решения/библиотеки stringify, и на следующем рисунке показана их производительность по сравнению с собственными методами:

родной, когда зеленыйJSON.stringify(), то видно, что производительность намного ниже, чем у этих библиотек. Итак, каково техническое обоснование резкого улучшения производительности?

2. СравнитеstringifyБыстрееstringify

Поскольку JavaScript — очень динамичный язык, для переменной типа Object имя ключа, значение ключа и тип значения ключа, которые она содержит, могут быть определены только во время выполнения. Поэтому выполнитеJSON.stringify()Будет много работы. Мы ничего не можем сделать, чтобы радикально оптимизировать, не зная об этом.

Итак, если мы знаем имя ключа и информацию о значении ключа в этом объекте, то есть знаем информацию о его структуре, поможет ли это?

См. пример:

Следующий объект,

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метод?

  1. Информация о структуре объекта должна быть определена в первую очередь;
  2. Создавайте «настраиваемые» объекты для объектов этой структуры на основе информации об их структуре.stringifyметод, который фактически генерирует результат путем конкатенации строк;
  3. Наконец, используйте «пользовательский» метод, чтобы преобразовать объект в строку.

Это также процедура большинства библиотек ускорения 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

Рисунок ниже основан наfast-json-stringifyПредоставил результаты тестов, разобрался со сравнением производительности.

Видно, что в большинстве сценариев наблюдается улучшение производительности в 2-5 раз.

3.1.1. Как определяется схема

fast-json-stringify используетJSON Schema Validation для определения формата данных объекта (JSON). Структура, определяемая самой схемой, также имеет формат JSON, например объект

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

Соответствующая схема:

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

Его правила определения схемы богаты, для конкретного использования, пожалуйста, обратитесь кAjvЭта библиотека проверки JSON.

3.1.2 Генерация метода stringify

fast-json-stringify будет генерировать фактическую строку кода функции путем объединения в соответствии с только что определенной схемой, а затем использоватьКонструктор функцийСоответствующая функция stringify динамически генерируется во время выполнения.

В кодогенерации в первую очередь будут внедряться различные предопределенные методы инструмента.Разные схемы в этой части одинаковые:

var code = `
    'use strict'
  `

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

Во-вторых, конкретный код функции stringify будет сгенерирован в соответствии с конкретным содержимым, определенным схемой. Способ генерации тоже относительно прост: обходом схемы.

При обходе схемы в соответствии с заданным типом вставьте соответствующую функцию инструмента в соответствующий код для преобразования ключ-значение. Например, в приведенном выше примере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Переменная содержит строку кода конечного сгенерированного тела функции. Так как в определении схемыnameдляstringtype и не является пустым, поэтому он будет в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

slow-json-stringifyХотя имя «медленное», на самом деле это «быстрая» библиотека stringify (название непослушное).

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

Его реализация проще, чем вышеупомянутый fast-json-stringify, и идея очень умная. В то же время этоВо многих сценариях эффективность будет выше, чем у fast-json-stringify..

3.2.1. Как определяется схема

Определение схемы slow-json-stringify более естественно и просто, в основном заменяя ключевые значения описаниями типов. Или на примере объекта выше схема станет

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

Очень интуитивно понятно.

3.2.2 Генерация метода stringify

Я не знаю, заметили ли вы

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

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

Похожа ли структура схемы и исходного объекта?

Умная вещь в этой схеме заключается в том, что после этого определения мы можем сначала поместить схемуJSON.stringifyЩелкните, затем «вычтите» все значения типа, и, наконец, нас ждет заполнение фактического значения непосредственно в объявлении типа, соответствующем схеме.

Как это работает?

Во-первых, вы можете напрямую вызвать схему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'
]

Наконец, поскольку сопоставление имен свойств и путей доступа хранится в карте, вы можете получить доступ к значению свойства в объекте в соответствии с реквизитом, выполнить цикл по массиву и объединить его с соответствующими фрагментами.

С точки зрения размера кода и реализации это решение будет легче и изобретательнее, и ему не нужно динамически генерировать или выполнять функции через Function, eval и т. д.

4. Резюме

Хотя реализация разных библиотек отличается, из общей идеи способ достижения высокой производительности stringify один и тот же:

  1. Разработчик определяет схему объекта JSON;
  2. Библиотека stringify генерирует соответствующий шаблонный метод по схеме, а шаблонный метод будет выполнять склейку строк атрибутов и значений (очевидно, эффективность доступа к атрибутам и склейки строк гораздо выше);
  3. Наконец, разработчик вызывает возвращенный метод для преобразования объекта в строку.

В конечном счете, он по существу предшествует оптимизации и анализу со статической структурной информацией.

Tips

Наконец, я хотел бы упомянуть

  • Все бенчмарки можно использовать только в качестве справки, есть ли улучшение производительности и насколько оно улучшено, или рекомендуется протестировать в реальном бизнесе;
  • Fast-json-stringify использует конструктор Function, поэтому рекомендуется не использовать пользовательский ввод напрямую в качестве схемы, чтобы предотвратить некоторые проблемы с безопасностью.

Что ж, этот выпуск «Roaming Github» уже здесь. В этой серии мы будем время от времени смотреть, общаться и узнавать об интересных проектах на github, не только для изучения некоторых технических моментов, но и для понимания технического мышления автора.Приглашаем к вниманию заинтересованных партнеров.