Рекомендую подписаться↓

предисловие

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

В настоящее время частота обновления основных мониторов составляет 60 Гц, то есть один кадр составляет 16 мс, поэтому рекомендуется, чтобы анимация воспроизводилась в течение 16 мс, ответ на действие пользователя должен быть менее 100 мс, а страница открывалась до начала. представления контента должно быть менее 1000 мс.

-- Согласно RAIL, модели производительности, воспринимаемой пользователями, предложенной командой Chrome.

Вышеприведенное приложение является оптимальной моделью взаимодействия с пользователем, предложенной командой Google.С точки зрения работы js общий смысл заключается в том, чтобы попытаться обеспечить выполнение каждой js-задачи в кратчайшие сроки.

сценарий дела

В современных веб-программах необходимость экспорта данных и отчетов стала очень распространенной. Объем экспортируемых данных становится все больше и сложнее, и в большинстве случаев во внешнем интерфейсе также может потребоваться преобразование наиболее распространенных полей времени, поэтому невозможно избежать обхода исходных данных. Теперь возьмем в качестве примера экспорт отчетов данных мониторинга различных факторов сайта:

требования к формату отчета

  1. Каждая часть данных содержит несколько элементов данных фактора, и каждый элемент данных фактора содержит данные мониторинга измененного фактора и соответствующий уровень оценки;
  2. Требуется экспортировать 90-дневные почасовые данные предыдущего квартала, а источник данных около 2100 (с условиями запроса на подкачку);
  3. Для отчета требуется формат времени YYYY年MM月DD日 HH时(например: 23:00 25 декабря 2020 г.), а содержание каждого фактора — это данные фактора + уровень фактора (например: 2,36(I)).

Сетка источника данных

Формат данных, возвращаемых серверной частью, следующий.

{
        "dateTime""2021-06-05 14:00:00",
        "name""站点一",
        "factorDatas": [
            {"code""w01010""grade"1"value"26.93},
            {"code""w666666""grade"1"value"1.26}
        ]
}

Базовая обработка источника данных

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

  1. Большой цикл должен обрабатывать поле dateTime;
  2. В маленьком цикле необходимо зациклить поле factorDatas, запросить имя оценки, соответствующее оценке, и, наконец, формат, необходимый для отчета, будет склеен.

Бросание кирпичей и привлечение нефрита

Простая реализация

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

Обычный процесс разработки, конечно, заключается в использовании цикла for для непрерывного вызова интерфейса подкачки для непрерывного запроса данных до тех пор, пока запрос данных не будет завершен, а затем обработки каждой строки данных в едином цикле. Для удобства обработки данных для некоторых публичных методов выделен отдельный класс инструмента:

class UtilsSerice {
    /**
     * 获取水质类别信息
     * @param waterType
     * @param keyValue
     * @param keyName?
     */

    static async getGradeInfo(waterType: WaterTypeStringEnum, keyValue: string | number, keyName?: string): Promise<WaterGrade | null | undefined> {
        // 缓存中数据的 key 
        const flagId: string = waterType + keyValue;
        // 缓存中有对应的值,直接返回
        if (TEMP_WATER_GRADE_MAP.get(flagId)) {
            return TEMP_WATER_GRADE_MAP.get(flagId);
        }
        // 获取等级列表
        const gradeList: WaterGrade[] = await this.getEnvData(waterType);
        // 查询等级值对应的等级信息
        const gradeInfo: WaterGrade = gradeList.find((item: WaterGrade) => {
            const valueName: string | number | undefined = keyName === 'id' ? 'id' : item.hasOwnProperty('value') ? 'value' : 'level';
            return item[valueName] === keyValue;
        }) as WaterGrade;
        // 将查询到的等级信息缓,方便下一次查询该等级时直接返回
        if (gradeInfo) {
            TEMP_WATER_GRADE_MAP.set(flagId, gradeInfo);
        }
        return gradeInfo;
    }

}

Логика экспорта данных следующая:

// 假设 allList 已经是 2100 条数据集合
const allList = [{"dateTime""2021-06-05 14:00:00""code""sssss""name""站点一""factorDatas": [{"code""w01010""grade"1"value"26.93}, {"code""w666666""grade"1"value"1.26}]}]

const table: ObjectUnknown[] = [];
for (let i = 0; i < allList.length; i ++) {
    const rows = {...allList[i]}
    // 按需求处理时间格式
    rows['tiemStr'] = moment(allList[i].dateTime).format('YYYY年MM月DD日 HH时')
    for (let j = 0; j < allList[i].factorDatas.length; j ++) {
        const code = allList[i].factorDatas[j].code
        const value = allList[i].factorDatas[j].value
        const grade = allList[i].factorDatas[j].grade
        // 此处按需求异步获取等级数据----  此方法已经尽可能的做了性能优化
        const gradeStr = await UtilsSerice.getGradeInfo('surface', grade, 'value')
        rows[code] = `${value}(${gradeStr})`
    }
    table.push(rows)
    
}
const downConfig: ExcelDownLoadConfig = {
    tHeader: ['点位名称''接入编码''监测时间''因子1''因子2''因子2' ],
    bookType'xlsx',
    autoWidth80,
    filename`数据查询`,
    filterVal: ['name''code''tiemStr''w01010''w01011''w01012'],
    multiHeader: [],
    merges: []
};
// 此方法是通用的 excel 数据处理逻辑
const res: any = await ExcelService.downLoadExcelFileOfMain(table, downConfig);
const file = new Blob([res.data], { type'application/octet-stream' });
// 文件保存
saveAs(file, res.filename);

Поскольку поток движка JS является однопоточным и взаимоисключающим с потоком рендеринга графического интерфейса, при выполнении сложных вычислительных задач js интуитивное ощущение пользователя заключается в том, что система зависла, например, поле ввода не может быть введено, анимация останавливается, кнопка недействительна и т.д. Приведенный выше код может реализовать экспорт данных.Видно, что когда основной поток экспортирует данные, вращение изображения останавливается, и поле ввода больше не может быть введено.

картина

Я считаю, что как бы красноречива ни была партия А, для такой системы это неприемлемо.

проблемное мышление

Разработчики с небольшим опытом программирования более или менее понимают, что обход больших данных в цикле for блокирует выполнение других скриптов.Основываясь на этой идее, инженеры-разработчики с опытом оптимизации производительности, скорее всего, разобьют этот большой обход на несколько обходов. задачи выполняются с меньшим заиканием, и это решение также может в определенной степени решить проблему заикания.

Однако эта схема оптимизации с разделением времени и задач не подходит для обработки не всех больших данных, особенно с сильными зависимостями между фронтальными и задними данными, поэтому эта схема оптимизации пока не будет обсуждаться в этой статье. В этой статье речь пойдет о webWorker:

Это позволяет одновременно выполнять несколько сценариев JavaScript в веб-программе.Каждый поток выполнения сценария называется потоком, независимым друг от друга и управляемым механизмом JavaScript в браузере. Это позволит передавать сообщения на уровне потока. Делает возможным многопоточное программирование на веб-страницах.

-- Сообщество IMWeb

webWorker имеет несколько функций:

  1. Возможность работать в течение длительного времени (отзывчивый)
  2. Быстрый запуск и идеальное потребление памяти
  3. естественная среда песочницы

использование webWorker

Создайте

//创建一个Worker对象,并向它传递将在新线程中执行的脚本url
const worker = new Worker('worker.js');

коммуникация

// 发送消息
worker.postMessage({first:1,second:2});
// 监听消息
worker.onmessage = function(event){
    console.log(event)
};

разрушать

Рабочий процесс завершается в основном потоке и больше не может использоваться для передачи сообщений. Примечание. После завершения его нельзя повторно включить, его можно только создать.

worker.terminate();

Миграция функции экспорта

Далее поговорим о том, как мигрировать код экспорта данных в webWorker.Перед миграцией функции нам нужно разобраться с пререквизитами для экспорта данных:

1: WebWorker должен иметь возможность вызывать ajax для получения данных интерфейса
2: Сценарий excel.js должен быть загружен в webWorker
3: Функция saveAs в файле-сохранителе может вызываться нормально;

Основываясь на вышеуказанных условиях, мы обсудим их по порядку.Во-первых, повезло, что webWorker поддерживает инициирование данных ajax-запроса, во-вторых, интерфейс importScripts() предоставляется в webWorker, поэтому экземпляр Excel также может быть сгенерирован. в webWorker; третий пункт немного прискорбен, объекты DOM не могут использоваться в webWorker, а файл-сохранитель просто использует DOM, поэтому он может передавать данные в основной поток только после обработки данных в подпотоке, а основной поток выполняет операцию сохранения файла.

Сравнение схем

В настоящее время в отрасли существует множество решений для интеграции webWorker, вот простое сравнение (от фронтенд-команды Tencent):

проект Введение пакет сборки Инкапсуляция API низкого уровня Объявление межпотокового вызова Мониторинг доступности Простота расширения
рабочий-грузчик [1] Официальный Webpack, возможность упаковки исходного кода ✔️
обещание-рабочий [2] Инкапсулируйте базовый API для взаимодействия с Promise ✔️
комлинк [3] Команда Chrome, коммуникационная оболочка RPC ✔️ Одноименная функция (на основе Proxy)
рабочий-загрузчик [4] Текущий относительно полный план сообщества ✔️ ✔️ Одноименная функция (сгенерирована на основе AST)
сплавщик [5] Платформа связи Worker с высокой доступностью, ориентированная на транзакции Предоставляем скрипты сборки Связь ️ Контроллер одноименная функция (конвенция), декларация ТС Индикаторы полного контроля, полный контроль ошибок цикла пространство имен, скрипт генерации транзакций
веб-пакет5 [6] Используется для замены рабочего-загрузчика в webpack5 Предоставляем скрипты сборки

Основываясь на приведенном выше сравнении и моем личном предпочтении ts, в этом случае для интеграции webWorker используется сплав-воркер.Из-за проблемы с официальным пакетом npm его нельзя интегрировать за один раз, поэтому его можно интегрировать только вручную.

интеграция рабочих

Официальная документация по интеграции [7]

Во-первых, скопируйте исходный код основного базового взаимодействия с рабочими в каталог проекта src/worker.

декларативная сделка

Первый шаг — добавить транзакцию для экспорта данных в src/worker/common/action-type.ts.

export const enum TestActionType {
  MessageLog = 'MessageLog',
  // 声明数据导出的事务
  ExportStationReportData = 'ExportStationReportData'
  }

Запрос, объявление типа данных ответа

Объявите типы данных запроса и ответа в файле src/worker/common/payload-type.ts.

Запросить объявление типа данных для каждой транзакции, которая взаимодействует между потоками

export declare namespace WorkerPayload {
    namespace ExcelWorker {
        // 调用ExportStationReportData 导出数据时需要传这两个参数
        type ExportStationData = {
            factorList: SelectOptions[];
            accessCodes: string[];
        } & Transfer;
    }
}

Объявление типа данных ответа для каждой транзакции, которая взаимодействует между потоками

export declare namespace WorkerReponse {
    namespace ExcelWorker {
        type ExportStationData = {
            data: any;
        } & Transfer;
    }
}

логика основного потока

Создайте новый файл excel.ts в папке src/worker/main-thread для записи кода транзакции данных.

/**
 * 第四步:声明主线程业务逻辑代码
 * TODO
 */

export default class Excel extends BaseAction {
    protected threadAction: IMainThreadAction;
    /**
     * 导出监测点数据
     * @param payload
     */

    public async exportStationReportData(payload?: WorkerPayload.ExcelWorker.ExportStationData): Promise<WorkerReponse.ExcelWorker.ExportStationData> {
        return this.controller.requestPromise(TestActionType.ExportStationReportData, payload);
    }


    protected addActionHandler(): void {}
}

Реализация логики основного потока

Введите excel в src/worker/main-thread/index;

Основной поток объявляет пространство имен транзакций

// 只声明事务命名空间, 用于事务中调用其他命名空间的事务
export interface IMainThreadAction {
    // ....
    excel: Excel;
}

Основной поток объявляет создание транзакции

export default class MainThreadWorker implements IMainThreadAction {
    // ......
    public excel: Excel;
    public constructor(options: IAlloyWorkerOptions) {
        // .....
        this.excel = new Excel(this.controller, this);
    }
    // ........ 省略代码
}

логика дочернего потока

Создайте новый файл excel.ts в папке src/worker/worker-thread для записи кода транзакции данных.Этот файл является основной функцией экспорта данных.

Запрос данных, обработка данных

export default class Test extends BaseAction {
    protected threadAction: IWorkerThreadAction;

    protected addActionHandler(): void {
        this.controller.addActionHandler(TestActionType.ExportStationReportData, this.exportStationReportData.bind(this));
    }
    /**
     * 获取数据查询
     * @protected
     */

    @HttpGet('/list')
    protected async getDataList(@HttpParams() queryDataParams: QueryDataParams, factors?: SelectOptions[], @HttpRes() res?: any): Promise<{ total: number; list: TableRow[] }> {
        return {list: res.rows}
    }

    /**
     * 测试导出数据
     * @private
     */

    private async exportExcel(payload?: WorkerPayload.ExcelWorker.ExportExcel): Promise<any> {
        try {
            // worker 中引入 xlsx
            importScripts('https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.9/xlsx.core.min.js');
            const table: ObjectUnknown[] = [];
            for (let i = 0; i < allList.length; i ++) {
                const rows = {...allList[i]}
                // 按需求处理时间格式
                rows['tiemStr'] = moment(allList[i].dateTime).format('YYYY年MM月DD日 HH时')
                for (let j = 0; j < allList[i].factorDatas.length; j ++) {
                    const code = allList[i].factorDatas[j].code
                    const value = allList[i].factorDatas[j].value
                    const grade = allList[i].factorDatas[j].grade
                    // 此处按需求异步获取等级数据----  此方法已经尽可能的做了性能优化
                    const gradeStr = await UtilsSerice.getGradeInfo('surface', grade, 'value')
                    rows[code] = `${value}(${gradeStr})`
                }
                table.push(rows)

            }
            const downConfig: ExcelDownLoadConfig = {
                tHeader: ['点位名称''接入编码''监测时间''因子1''因子2''因子2' ],
                bookType'xlsx',
                autoWidth80,
                filename`数据查询`,
                filterVal: ['name''code''tiemStr''w01010''w01011''w01012'],
                multiHeader: [],
                merges: []
            };
            const res = await ExcelService.downLoadExcelFile(table, downConfig, (self as any).XLSX);
            // 由于之前提到的 worker 局限性(无法访问 DOM) 因此子线程中处理完 excel 所所需的对象后 将数据传递给主线程,由主线程进行数据导出
            //  普通 postMessage 时会进行 树的克隆,但此处处理完的数据可能会非常大,估计直接将进行 transfer 传输数据
            return {
                transferProps: ['data'],
                data: res.data,
                filename: res.filename,
            }
        } catch (e) {
            console.log(e);
        }
    }
}

Реализация логики дочернего потока

Введите excel в src/worker/worker-thread/index;

Основной поток объявляет пространство имен транзакций

// 只声明事务命名空间, 用于事务中调用其他命名空间的事务
export interface IWorkerThreadAction {
    // ....
    excel: Excel;
}

Дочерний поток объявляет создание транзакции

class WorkerThreadWorker implements IWorkerThreadAction {
    public excel: Excel
    // ... 省略代码
    public constructor() {
        this.controller = new Controller();
        this.excel = new Excel(this.controller, this);

        // ... 省略代码
    }
}

Пока функция экспорта полностью перенесена в дочерний поток.

вызов основного потока

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

class HomPage extends VueComponent {
    public created() {
        try {
            // 实例化一个子线程,并将其挂载在 window 上
            const alloyWorker = createAlloyWorker({
                workerName'alloyWorker--test',
                isDebugModetrue
            });
        }catch (e) {
            console.log(e);
        }

    }
    /**
     * 子线程数据导出
     * @private
     */

    private async exportExcelFile() {
        // 直接调用申明的方法就可以
        (window as any).alloyWorker.excel.exportStationReportData({
            factorList: factors,
            accessCodes: [{ accessCode'sss'name'测试监测点' }]
        }).then((res: any) => {
            // 大数据导出效果,子线程传回来的数据
            console.log(res);
            // 将子线程传回来的二进制数据转换为 Blob 方便文件保存
            const file = new Blob([res.data], { type'application/octet-stream' });
            // 保存文件
            saveAs(file, res.filename);
        });
    }
}

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

картина

Подвести итог

В приведенном выше коде используется реальный случай спроса, чтобы убедиться, что webWorker может значительно улучшить взаимодействие с пользователем. Эта потребность, вероятно, не так велика в большинстве случаев разработки, но иногда она возникает.

Конечно, webWorker не единственное решение, в случае того же объема вычислений, вычисление в подпотоке будет не намного быстрее основного потока, и даже не медленнее основного потока, поэтому только некоторые приложения которые не требуют своевременной обратной связи, могут быть использованы Расчет помещается в дочерний поток для расчета. Если вы хотите просто повысить эффективность вычислений, вы можете начать только с алгоритмов или использовать WebAssembly для повышения эффективности вычислений .

Ссылаться на

  1. Web_Workers_API [8]
  2. рабочие данные [9]
  3. сплавщик [10]

использованная литература

[1]

рабочий-загрузчик:  https://github.com/webpack-contrib/worker-loader

[2]

обещание-работник:  https://github.com/nolanlawson/promise-worker

[3]

комлинк:  https://github.com/GoogleChromeLabs/comlink

[4]

workerize-загрузчик:  https://github.com/developit/workerize-loader

[5]

сплав-рабочий:  https://github.com/AlloyTeam/alloy-worker

[6]

webpack5:  https://webpack.docschina.org/guides/web-workers/#root

[7]

Официальная документация по интеграции:  https://github.com/developit/workerize-loader

[8]

Web_Workers_API:  https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API

[9]

Информация о работнике:  https://cloud.tencent.com/developer/information/webworker .

[10]

сплав-рабочий:  https://github.com/AlloyTeam/alloy-worker

Автор: сын убывающей луны

https://juejin.cn/post/6970336963647766559

- ЭОФ -
Рекомендуемое чтение   Нажмите на название, чтобы перейти

1. Service Worker: сделайте свое веб-приложение потрясающим

2. Wasm открывает безграничные возможности для веб-разработки

3. Node.js может реализовать кроссплатформенную совместимость HTTP-запросов с Интернетом!

Думаете, эта статья была вам полезна? пожалуйста, поделитесь с большим количеством людей

Рекомендуется обратить внимание на «Front-end Daquan» для улучшения навыков работы с интерфейсом.

Лайк и просмотр - самая большая поддержка ❤️