Recomendar seguir↓

prefacio

Con la complejidad gradual de los escenarios de aplicaciones front-end, el procesamiento de big data que lo acompaña es inevitable. Entonces, hoy, tomemos un escenario de aplicación real como ejemplo para hablar sobre cómo procesar big data a través de subprocesos en el front-end.

En la actualidad, la frecuencia de actualización de los monitores convencionales es de 60 Hz, es decir, un cuadro es de 16 ms. Por lo tanto, se recomienda que la animación se reproduzca dentro de los 16 ms, la respuesta de la acción del usuario debe ser inferior a 100 ms y la página se abre al principio. de la presentación del contenido debe ser inferior a 1000ms.

-- Según RAIL, un modelo de rendimiento percibido por el usuario propuesto por el equipo de Chrome.

La aplicación anterior es el modelo de experiencia de usuario óptimo propuesto por el equipo de Google.Desde la perspectiva de la operación js, el significado general es tratar de garantizar que cada tarea js se ejecute en el menor tiempo posible.

de los casos

En los programas web modernos, la necesidad de exportar datos e informes se ha vuelto muy común. La cantidad de datos exportados es cada vez más grande y más compleja, y es posible que el front-end también deba convertir los campos de tiempo más comunes en la mayoría de los casos, por lo que no se puede evitar el cruce de los datos de origen. Ahora tome como ejemplo la exportación de informes de datos de monitoreo de varios factores de un sitio:

requisitos de formato de informe

  1. Cada dato contiene varios elementos de datos de factores, y cada elemento de datos de factores contiene los datos de seguimiento del factor modificado y el nivel de evaluación correspondiente;
  2. Se requiere exportar los datos por hora de 90 días del trimestre anterior, y la fuente de datos es aproximadamente 2100 (con condiciones de consulta de paginación);
  3. El informe requiere el formato de hora YYYY年MM月DD日 HH时(por ejemplo: 23:00 el 25 de diciembre de 2020), y el contenido de cada factor es datos de factor + nivel de factor (por ejemplo: 2.36(I)).

Cuadrícula de fuente de datos

El formato de los datos devueltos por el backend es el siguiente

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

Procesamiento básico de fuentes de datos

De acuerdo con los requisitos de exportación de informes, no se puede evitar el cruce de estos más de 2000 datos, e incluso puede haber procesamiento de bucles grandes que anidan bucles pequeños.

  1. El bucle grande necesita procesar el campo de fecha y hora;
  2. En el bucle pequeño, el campo factorDatas necesita ser enlazado, se consulta el nombre de calificación correspondiente a la calificación y, finalmente, se separa el formato requerido para el informe.

Tirando ladrillos y atrayendo jade

Implementación sencilla

El siguiente código es solo un código de simulación, la interfaz predeterminada ha completado la carga de todos los datos

El proceso de desarrollo normal, por supuesto, es usar el bucle for para llamar continuamente a la interfaz de paginación para consultar continuamente los datos hasta que se complete la consulta de datos y luego procesar cada fila de datos en un bucle unificado. Para facilitar el procesamiento de datos, se dibuja una clase de herramienta separada para algunos métodos públicos:

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;
    }

}

La lógica de exportación de datos es la siguiente:

// 假设 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);

Dado que el subproceso del motor JS es de un solo subproceso y se excluye mutuamente con el subproceso de representación de la GUI, al realizar tareas informáticas JS complejas, la sensación intuitiva del usuario es que el sistema está atascado, como que no se puede ingresar el cuadro de entrada, la animación se detiene, el el botón no es válido, etc. El código anterior puede realizar la exportación de datos.Se puede ver que cuando el subproceso principal exporta los datos, la rotación de la imagen se detiene y el cuadro de entrada ya no se puede ingresar.

imagen

Yo creo que por muy elocuente que sea el Partido A, es inaceptable para un sistema así.

problema de pensamiento

Los desarrolladores con un poco de experiencia en programación entenderán más o menos que el recorrido del bucle for de big data bloquea la ejecución de otros scripts. Basándose en esta idea, los ingenieros de desarrollo con experiencia en la optimización del rendimiento probablemente dividirán este gran recorrido en varios recorridos. Pequeño las tareas vienen con menos tartamudeo, y esta solución también puede resolver el problema de la tartamudez hasta cierto punto.

Sin embargo, este esquema de optimización de división de tiempo y división de tareas no es adecuado para todos los procesamientos de big data, especialmente aquellos con fuertes dependencias entre los datos anteriores y posteriores.Este esquema de optimización no se discutirá en este artículo por el momento. Este artículo hablará sobre webWorker:

Permite la ejecución concurrente de múltiples scripts de JavaScript en un programa Web. Cada flujo de ejecución de script se denomina hilo, son independientes entre sí y son administrados por el motor de JavaScript en el navegador. Esto habilitará la comunicación de mensajes a nivel de hilo. Hace posible la programación de subprocesos múltiples en páginas web.

-- Comunidad IMWeb

webWorker tiene varias características:

  1. Capacidad para correr durante mucho tiempo (sensible)
  2. Inicio rápido y consumo de memoria ideal
  3. entorno de caja de arena natural

uso de webworker

crear

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

comunicación

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

destruir

El trabajador finaliza en el subproceso principal y ya no se puede usar para pasar mensajes. Nota: Una vez terminado, no se puede volver a habilitar, solo se puede crear.

worker.terminate();

Migración de la función de exportación

A continuación, hablemos sobre cómo migrar el código de esta parte de la exportación de datos a webWorker. Antes de la migración de la función, debemos resolver los requisitos previos para la exportación de datos:

1: el trabajador web debe poder llamar a ajax para obtener los datos de la interfaz,
2: el script de excel.js debe cargarse en el trabajador web,
3: la función saveAs en el protector de archivos se puede llamar normalmente;

Sobre la base de las condiciones anteriores, las discutiremos una por una. El primer punto tiene suerte de que webWorker admita el inicio de datos de solicitud ajax; el segundo punto es que la interfaz importScripts() se proporciona en webWorker, por lo que también se puede generar una instancia de Excel en webWorker; el tercer punto es un poco lamentable, los objetos DOM no se pueden usar en webWorker, y el protector de archivos solo usa DOM, por lo que solo puede pasar los datos al subproceso principal después de procesar los datos en el subproceso, y el hilo principal realiza la operación de guardado de archivos.

Comparación de esquemas

En la actualidad, existen muchas soluciones para integrar webWorker en la industria. La siguiente es una comparación simple (del equipo front-end de Tencent):

proyecto Introducción paquete de compilación Encapsulación de API de bajo nivel Declaración de llamada entre subprocesos Monitoreo de disponibilidad Facilidad de extensibilidad
trabajador-cargador [1] Webpack oficial, capacidad de empaquetado de código fuente ✔️
trabajador de la promesa [2] Encapsule la API básica para la comunicación de Promise ✔️
comunicador [3] Equipo de Chrome, contenedor RPC de comunicación ✔️ Función del mismo nombre (basada en Proxy)
trabajadorizar-cargador [4] El plan actual relativamente completo de la comunidad ✔️ ✔️ Función del mismo nombre (generada en base a AST)
trabajador de la aleación [5] Marco de comunicación Worker de alta disponibilidad orientado a transacciones Proporcionar scripts de compilación Comunicación ️ Controlador función del mismo nombre (basada en convenciones), declaración TS Indicadores de monitoreo completo, monitoreo de errores de ciclo completo espacio de nombres, secuencia de comandos de generación de transacciones
paquete web5 [6] Se usa para reemplazar el cargador de trabajadores en webpack5 Proporcionar scripts de compilación

Basado en la comparación anterior y mi preferencia personal por ts, este caso usa alloy-worker para integrar webWorker.Debido al problema con el paquete oficial npm, no se puede integrar en un solo lugar, por lo que solo se puede integrar manualmente.

integración de los trabajadores

Documentación oficial de integración [7]

En primer lugar, copie el código fuente básico básico de comunicación del trabajador en el directorio del proyecto src/worker.

transacción declarativa

El primer paso es agregar la transacción para la exportación de datos en src/worker/common/action-type.ts.

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

Solicitud, declaración de tipo de datos de respuesta

Declare los tipos de datos de solicitud y respuesta en el archivo src/worker/common/payload-type.ts.

Solicite la declaración del tipo de datos para cada transacción que se comunica a través de subprocesos

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

Declaración de tipo de datos de respuesta para cada transacción que se comunica a través de subprocesos

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

lógica del hilo principal

Cree un nuevo archivo excel.ts en src/worker/main-thread para escribir el código de transacción de datos.

/**
 * 第四步:声明主线程业务逻辑代码
 * 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 {}
}

Instanciación de la lógica del subproceso principal

Introduzca excel en src/worker/main-thread/index;

El hilo principal declara el espacio de nombres de la transacción

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

El hilo principal declara la creación de instancias de transacciones.

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

lógica de subprocesos secundarios

Cree un nuevo archivo excel.ts en src/worker/worker-thread para escribir el código de transacción de datos.Este archivo es la función principal de exportación de datos.

Solicitud de datos, procesamiento de datos

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);
        }
    }
}

Creación de instancias de lógica de subprocesos secundarios

Introduzca excel en src/worker/worker-thread/index;

El hilo principal declara el espacio de nombres de la transacción

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

El subproceso secundario declara la creación de instancias de transacciones

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

        // ... 省略代码
    }
}

Hasta ahora, la función de exportación se ha migrado por completo al subproceso secundario.

llamada de hilo principal

También es muy simple para el subproceso principal llamar a la función de exportación de datos.Primero, se crea una instancia de un subproceso secundario, y luego la lógica de cálculo compleja se puede enviar felizmente al subproceso secundario, similar a esto.

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);
        });
    }
}

El efecto es el siguiente, puede sentir claramente que la página no se atasca durante el proceso de exportación de datos.

imagen

Resumir

以上代码中以一个真实的需求案例验证了 webWorker 对用户体验的提升是非常大的。这种需求在大多数的开发中可能也不多,但偶尔也会有。

当然 webWorker 也并非是唯一解,在同等计算量的情况下,在子线程中做计算并不会比主线程快多少, 甚至会比主线程慢,因此只能将一些对及时反馈要求不高的计算放到子线程中计算。如果想单纯的提高计算效率,那只能从算法上入手或者使用 WebAssembly 来提高计算效率

参考

  1. Web_Workers_API[8]
  2. worker资料[9]
  3. alloy-worker[10]

参考资料

[1]

worker-loader: https://github.com/webpack-contrib/worker-loader

[2]

promise-worker: https://github.com/nolanlawson/promise-worker

[3]

comlink: https://github.com/GoogleChromeLabs/comlink

[4]

workerize-loader: https://github.com/developit/workerize-loader

[5]

alloy-worker: 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/zh-CN/docs/Web/API/Web_Workers_API

[9]

worker资料: https://cloud.tencent.com/developer/information/webworker

[10]

alloy-worker: https://github.com/AlloyTeam/alloy-worker

作者:残月公子

https://juejin.cn/post/6970336963647766559

- EOF -
推荐阅读  点击标题可跳转

1、Service Worker:让你的 Web 应用牛逼起来

2、Wasm 为 Web 开发带来无限可能

3、Node.js 可以和 Web 实现 HTTP 请求的跨平台兼容了!

觉得本文对你有帮助?请分享给更多人

推荐关注「前端大全」,提升前端技能

Dar like y ver es el mayor apoyo ❤️