↓ Рекомендую подписаться↓
С постепенным усложнением сценариев клиентских приложений сопутствующая обработка больших данных неизбежна. Итак, сегодня давайте возьмем реальный сценарий приложения в качестве примера, чтобы поговорить о том, как обрабатывать большие данные через подпотоки во внешнем интерфейсе.
В настоящее время частота обновления основных мониторов составляет 60 Гц, то есть один кадр составляет 16 мс, поэтому рекомендуется, чтобы анимация воспроизводилась в течение 16 мс, ответ на действие пользователя должен быть менее 100 мс, а страница открывалась до начала. представления контента должно быть менее 1000 мс.
-- Согласно RAIL, модели производительности, воспринимаемой пользователями, предложенной командой Chrome.
Вышеприведенное приложение является оптимальной моделью взаимодействия с пользователем, предложенной командой Google.С точки зрения работы js общий смысл заключается в том, чтобы попытаться обеспечить выполнение каждой js-задачи в кратчайшие сроки.
сценарий дела
В современных веб-программах необходимость экспорта данных и отчетов стала очень распространенной. Объем экспортируемых данных становится все больше и сложнее, и в большинстве случаев во внешнем интерфейсе также может потребоваться преобразование наиболее распространенных полей времени, поэтому невозможно избежать обхода исходных данных. Теперь возьмем в качестве примера экспорт отчетов данных мониторинга различных факторов сайта:
требования к формату отчета
-
Каждая часть данных содержит несколько элементов данных фактора, и каждый элемент данных фактора содержит данные мониторинга измененного фактора и соответствующий уровень оценки; -
Требуется экспортировать 90-дневные почасовые данные предыдущего квартала, а источник данных около 2100 (с условиями запроса на подкачку); -
Для отчета требуется формат времени 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 фрагментов данных, и может даже выполняться обработка больших циклов, вложенных в маленькие циклы.
-
Большой цикл должен обрабатывать поле dateTime; -
В маленьком цикле необходимо зациклить поле 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',
autoWidth: 80,
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 имеет несколько функций:
-
Возможность работать в течение длительного времени (отзывчивый) -
Быстрый запуск и идеальное потребление памяти -
естественная среда песочницы
использование 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',
autoWidth: 80,
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',
isDebugMode: true
});
}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 для повышения эффективности вычислений .
Ссылаться на
-
Web_Workers_API [8] -
рабочие данные [9] -
сплавщик [10]
использованная литература
рабочий-загрузчик: 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» для улучшения навыков работы с интерфейсом.
Лайк и просмотр - самая большая поддержка ❤️