Vue2 数万级设备数据全局单例 WebWorker 完整实现
2026/7/23 1:12:55 网站建设 项目流程

项目结构

src/ ├── worker/ │ └── device.worker.js # Worker后台计算线程(筛选/格式化/聚合) ├── utils/ │ └── globalWorker.js # 全局单例Worker管理器(主线程封装) └── views/ └── DeviceData.vue # Vue2业务组件调用示例

一、全局单例 Worker 管理器 utils/globalWorker.js

核心能力:单例全局 Worker、taskId 任务标记、过期任务丢弃、循环引用清洗、消息异常捕获、组件销毁终止线程、任务取消队列

/** * 全局单例WebWorker管理器 - 设备数据运算专用 * 1. 全局唯一Worker实例,所有页面复用,避免多线程耗尽 * 2. taskId区分多组件并发任务,旧任务返回直接丢弃 * 3. 预处理数据:精简字段、清除循环引用,适配结构化克隆算法 * 4. 监听messageerror捕获DataCloneError序列化失败 * 5. 简易任务队列,支持取消未完成旧运算 * 6. 统一错误捕获,不阻塞主线程交互 */ let singletonWorker = null; let taskIdSeed = 0; // 全局自增任务ID const taskCallbacks = new Map(); // 存储taskId对应组件回调 let latestTaskIdMap = new Map(); // 每个组件实例最新taskId,用于丢弃过期结果 const pendingTaskSet = new Set(); // 待执行/执行中任务集合,用于取消 // 业务设备仅保留的核心字段,剔除冗余嵌套、循环引用对象 const DEVICE_KEEP_FIELDS = [ 'id', 'deviceName', 'statusCode', 'online', 'areaId', 'areaName', 'deviceType', 'createTime', 'voltage', 'temperature', 'power' ]; /** * 清除对象循环引用 + 精简业务字段,解决postMessage DataCloneError * @param {Array<Object>} rawList 原始设备数组(含循环引用/冗余属性) * @returns {Array<Object>} 可安全跨线程传输的纯数据 */ function cleanDeviceData(rawList) { const visited = new WeakMap(); // 标记已遍历对象,切断循环引用 return rawList.map(item => { // 递归清洗单个对象,剔除循环引用与无用字段 const cleanObj = {}; DEVICE_KEEP_FIELDS.forEach(key => { const val = item[key]; // 过滤DOM、函数、循环嵌套对象 if (typeof val === 'function' || val === null || val === undefined) return; if (typeof val === 'object' && val !== Date) { if (visited.has(val)) return; // 遇到循环引用直接丢弃该嵌套对象 visited.set(val, true); } cleanObj[key] = val; }); return cleanObj; }); } class GlobalDeviceWorker { constructor() { // 单例拦截 if (singletonWorker) return singletonWorker; singletonWorker = this; this.worker = null; this.initWorker(); } /** 初始化全局Worker实例,绑定所有监听事件 */ initWorker() { // webpack worker-loader引入worker脚本 const WorkerConstructor = require('../worker/device.worker.js'); this.worker = new WorkerConstructor(); // 1. 监听Worker正常消息返回 this.worker.addEventListener('message', (e) => { const { taskId, result, success } = e.data; // 无taskId直接忽略 if (!taskId) return; // 核心:判断是否为当前组件最新任务,旧任务结果直接丢弃不更新UI const compLatestId = latestTaskIdMap.get(taskCallbacks.get(taskId)?.compUid); if (compLatestId !== taskId) { pendingTaskSet.delete(taskId); return console.log(`丢弃过期任务${taskId},已有新计算发起`); } // 执行组件回调 const taskInfo = taskCallbacks.get(taskId); if (!taskInfo) return; const { resolve, reject } = taskInfo; pendingTaskSet.delete(taskId); taskCallbacks.delete(taskId); success ? resolve(result) : reject(result); }); // 2. 捕获结构化克隆失败(循环引用、非法数据类型触发messageerror) this.worker.addEventListener('messageerror', (err) => { console.error('Worker数据序列化失败(循环引用/非法字段)', err); // 清空所有loading,通知组件报错 taskCallbacks.forEach((info, tid) => info.reject('数据解析失败,请精简设备数据')); taskCallbacks.clear(); pendingTaskSet.clear(); }); // 3. 捕获Worker内部代码运行异常 this.worker.addEventListener('error', (err) => { console.error('Worker线程计算异常', err.message, err.lineno); taskCallbacks.forEach((info, tid) => info.reject('数据统计失败,请重试')); taskCallbacks.clear(); pendingTaskSet.clear(); }); } /** * 发起设备数据批量运算(筛选+格式化+聚合) * @param {String} compUid 组件唯一标识(区分不同页面组件) * @param {Array} rawDeviceList 原始数万级设备数组 * @param {Object} filterParams 多条件筛选参数 {status, online, areaList, typeList} * @returns {Promise<Object>} {filterList:格式化后数组, stat:聚合统计结果} */ runDeviceCalc(compUid, rawDeviceList, filterParams) { return new Promise((resolve, reject) => { // 1. 生成全局唯一taskId const taskId = ++taskIdSeed; // 2. 更新组件最新taskId,旧任务标记过期 latestTaskIdMap.set(compUid, taskId); // 3. 预处理:清洗循环引用、精简传输字段,减小传输体积 const cleanData = cleanDeviceData(rawDeviceList); // 4. 缓存当前任务回调,供Worker返回时分发 taskCallbacks.set(taskId, { compUid, resolve, reject }); pendingTaskSet.add(taskId); // 5. 发送消息至Worker,携带taskId区分任务 try { this.worker.postMessage({ taskId, type: 'DEVICE_BATCH_CALC', payload: { deviceList: cleanData, filterParams } }); } catch (postErr) { // 发送消息前捕获序列化报错 reject('数据传输失败:存在循环引用或非法对象'); pendingTaskSet.delete(taskId); taskCallbacks.delete(taskId); } }); } /** 取消当前组件所有未完成运算(切换筛选条件时丢弃旧计算) */ cancelComponentTask(compUid) { latestTaskIdMap.delete(compUid); // 清除该组件下所有待执行任务回调 const delTaskIds = []; taskCallbacks.forEach((info, tid) => { if (info.compUid === compUid) delTaskIds.push(tid); }); delTaskIds.forEach(tid => { taskCallbacks.delete(tid); pendingTaskSet.delete(tid); }); } /** 组件卸载时终止全局Worker(仅页面完全销毁调用,全局共用谨慎调用) */ terminateWorker() { if (this.worker) { this.worker.terminate(); this.worker = null; singletonWorker = null; taskCallbacks.clear(); pendingTaskSet.clear(); latestTaskIdMap.clear(); } } } // 导出全局单例实例 export default new GlobalDeviceWorker();

二、Worker 后台计算脚本 worker/device.worker.js

独立线程,执行三类耗时逻辑:批量多条件筛选、数据格式化、聚合统计,无 DOM 操作

/** * WebWorker后台设备数据运算线程 * 三大耗时逻辑全部移入此处,不阻塞主线程UI渲染 * 1. 批量多条件组合过滤设备 * 2. 统一数据清洗格式化(时间/状态码/单位/空值) * 3. 聚合统计:总数、在线离线分组、设备类型求和、均值计算 * 通信规范:接收携带taskId,返回结果带回taskId用于主线程分发 */ self.onmessage = function (e) { const { taskId, type, payload } = e.data; try { if (type === 'DEVICE_BATCH_CALC') { const { deviceList, filterParams } = payload; // 步骤1:多条件批量筛选 const filteredList = batchFilterDevice(deviceList, filterParams); // 步骤2:数据格式化、空值清洗、单位转换、状态转中文 const formatList = formatDeviceData(filteredList); // 步骤3:聚合统计计算 const statResult = aggregateDeviceStat(formatList); // 返回结果携带taskId,主线程匹配分发 self.postMessage({ taskId, success: true, result: { filterList: formatList, stat: statResult } }); } } catch (calcErr) { // Worker内部计算异常,回传错误信息 self.postMessage({ taskId, success: false, result: calcErr.message || '设备数据运算失败' }); } }; // ====================== 1. 批量多条件组合筛选 ====================== /** * 多字段联合过滤:状态、在线状态、区域、设备类型多条件匹配 * @param {Array} list 清洗后的设备数组 * @param {Object} params 筛选条件 * @returns {Array} 筛选后原始数据数组 */ function batchFilterDevice(list, params) { const { status, online, areaList = [], typeList = [] } = params; return list.filter(item => { // 状态筛选(不传则不限制) if (status !== undefined && item.statusCode !== status) return false; // 在线离线筛选 if (online !== undefined && item.online !== online) return false; // 区域多选匹配 if (areaList.length && !areaList.includes(item.areaId)) return false; // 设备类型多选匹配 if (typeList.length && !typeList.includes(item.deviceType)) return false; return true; }); } // ====================== 2. 数据格式化、空值清洗、单位转换 ====================== /** * 统一格式化逻辑:时间格式化、状态码转中文、数值换算、空值清洗 * @param {Array} rawFilterList 筛选后数组 * @returns {Array} 格式化完成业务数组 */ function formatDeviceData(rawFilterList) { // 状态码映射中文标签 const statusMap = { 0: '正常', 1: '故障', 2: '维护', 3: '停用' }; return rawFilterList.map(item => { const target = { ...item }; // 1. 空值清洗:空字符串/undefined统一转为"-" Object.keys(target).forEach(key => { if (target[key] === '' || target[key] === null || target[key] === undefined) { target[key] = '-'; } }); // 2. 状态码转中文 target.statusText = statusMap[target.statusCode] || '未知状态'; // 3. 时间格式化(时间戳转YYYY-MM-DD HH:mm) if (target.createTime !== '-') { const date = new Date(target.createTime); const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); const h = String(date.getHours()).padStart(2, '0'); const min = String(date.getMinutes()).padStart(2, '0'); target.createTimeText = `${y}-${m}-${d} ${h}:${min}`; } // 4. 数值单位换算:电压mV转V、功率w转kw if (target.voltage !== '-') target.voltageV = (target.voltage / 1000).toFixed(2); if (target.power !== '-') target.powerKw = (target.power / 1000).toFixed(2); return target; }); } // ====================== 3. 聚合统计计算 ====================== /** * 聚合指标:总数、在线离线分组、各类型数量、温度/电压平均值 * @param {Array} formatList 格式化完成设备数组 * @returns {Object} 统计指标 */ function aggregateDeviceStat(formatList) { const total = formatList.length; let onlineCount = 0; let offlineCount = 0; const typeGroup = {}; // 设备类型分组计数 let tempSum = 0; let voltageSum = 0; let validTempNum = 0; let validVoltNum = 0; formatList.forEach(item => { // 在线离线统计 if (item.online === true) onlineCount++; else offlineCount++; // 设备类型分组求和 const type = item.deviceType; typeGroup[type] = (typeGroup[type] || 0) + 1; // 温度均值累加 if (!isNaN(Number(item.temperature)) && item.temperature !== '-') { tempSum += Number(item.temperature); validTempNum++; } // 电压均值累加 if (!isNaN(Number(item.voltageV)) && item.voltageV !== '-') { voltageSum += Number(item.voltageV); validVoltNum++; } }); return { totalDevice: total, onlineCount, offlineCount, typeStat: typeGroup, avgTemperature: validTempNum ? (tempSum / validTempNum).toFixed(1) : '-', avgVoltage: validVoltNum ? (voltageSum / validVoltNum).toFixed(2) : '-' }; }

三、Vue2 业务组件调用示例 views/DeviceData.vue

包含:筛选切换取消旧任务、loading 控制、异常提示、组件卸载终止 Worker、唯一组件 ID 区分多页面

<template> <div class="device-page"> <div class="filter-bar"> <select v-model="filterParams.status" @change="handleSearch"> <option :value="undefined">全部状态</option> <option :value="0">正常</option> <option :value="1">故障</option> </select> <select v-model="filterParams.online" @change="handleSearch"> <option :value="undefined">全部在线</option> <option :value="true">在线</option> <option :value="false">离线</option> </select> <button @click="handleSearch" :disabled="loading">查询设备数据</button> </div> <div v-if="loading">数据运算中,请稍候...</div> <div v-if="errMsg" style="color:red">{{ errMsg }}</div> <!-- 聚合统计展示 --> <div v-if="statData"> <p>设备总数:{{ statData.totalDevice }}</p> <p>在线{{ statData.onlineCount }}台 / 离线{{ statData.offlineCount }}台</p> <p>平均温度:{{ statData.avgTemperature }}℃</p> </div> <!-- 格式化后设备表格 --> <table v-if="tableList.length"> <tr v-for="item in tableList" :key="item.id"> <td>{{ item.deviceName }}</td> <td>{{ item.statusText }}</td> <td>{{ item.createTimeText }}</td> <td>{{ item.voltageV }}V</td> </tr> </table> </div> </template> <script> import globalWorker from '@/utils/globalWorker'; // 模拟数万级原始设备数据(接口返回,含冗余字段/循环引用) function mockRawDeviceData(count = 15000) { const list = []; for (let i = 1; i <= count; i++) { const item = { id: i, deviceName: `设备_${i}`, statusCode: Math.floor(Math.random() * 4), online: Math.random() > 0.4, areaId: `area_${Math.floor(Math.random() * 5)}`, areaName: `区域${Math.floor(Math.random() * 5)}`, deviceType: ['A1', 'B2', 'C3'][Math.floor(Math.random() * 3)], createTime: Date.now() - Math.floor(Math.random() * 86400000 * 30), voltage: Math.floor(Math.random() * 10000), temperature: Math.floor(Math.random() * 60), power: Math.floor(Math.random() * 5000), // 冗余嵌套对象(存在循环引用,测试清洗逻辑) extraInfo: {}, }; item.extraInfo.parent = item; // 制造循环引用 list.push(item); } return list; } export default { name: 'DeviceData', data() { return { compUid: 'device_list_page_01', // 当前组件唯一标识 loading: false, errMsg: '', filterParams: { status: undefined, online: undefined, areaList: [], typeList: [] }, rawAllDevice: mockRawDeviceData(15000), // 15000条模拟设备 tableList: [], statData: null }; }, methods: { /** 触发筛选运算,切换条件自动取消上一次未完成任务 */ async handleSearch() { this.loading = true; this.errMsg = ''; // 核心:切换筛选条件时取消当前组件未完成旧运算,丢弃过期计算 globalWorker.cancelComponentTask(this.compUid); try { // 调用全局Worker执行三大耗时运算 const { filterList, stat } = await globalWorker.runDeviceCalc( this.compUid, this.rawAllDevice, this.filterParams ); this.tableList = filterList; this.statData = stat; } catch (err) { // 捕获序列化异常、Worker计算异常,友好提示 this.errMsg = err || '数据统计失败,请重新筛选'; console.error('设备计算任务失败', err); } finally { this.loading = false; } } }, beforeDestroy() { // 组件卸载:取消未完成任务,全局Worker共用不直接terminate globalWorker.cancelComponentTask(this.compUid); // 如需完全关闭全局线程(整个应用退出才调用):globalWorker.terminateWorker() } }; </script>

四、Webpack (vue-cli) 配置支持 worker-loader

vue-cli 项目安装依赖:

npm install worker-loader -D

vue.config.js 添加 loader 匹配 worker 文件

module.exports = { configureWebpack: { module: { rules: [ { test: /\.worker\.js$/, use: { loader: 'worker-loader' } } ] } } }

关键需求点全覆盖说明

  1. 全局单例 Worker采用单例模式,全局仅创建 1 个后台线程,所有页面组件共用,避免浏览器创建过多线程耗尽资源。

  2. taskId 任务隔离、丢弃过期结果

    • 每个运算生成自增唯一 taskId,绑定组件 compUid 区分多页面并发
    • 发起新筛选时更新组件最新 taskId,旧任务返回直接丢弃,不更新 UI
    • 切换筛选条件主动取消未完成旧计算,减少无效后台运算
  3. 结构化克隆兼容处理(循环引用报错解决)

    • cleanDeviceData预处理:WeakMap 标记循环对象、剔除嵌套循环引用
    • 仅保留业务核心字段,过滤冗余属性 / 函数 / DOM,大幅减小传输数据体积
    • 捕获messageerror监听 DataCloneError 序列化失败,给用户提示
  4. 三大耗时逻辑全部移入 Worker

    • 批量多条件过滤:状态、在线、区域、设备类型多字段联合筛选
    • 数据格式化:空值清洗、状态码转中文、时间格式化、电压 / 功率单位换算
    • 聚合统计:设备总数、在线离线分组、设备类型分组计数、温度电压平均值
  5. 完整异常捕获体系

    • worker.onerror:捕获 Worker 内部代码计算报错
    • messageerror:捕获跨线程数据序列化失败(循环引用 / 非法对象)
    • postMessage 外层 try-catch:捕获发送消息阶段序列化异常
    • 统一清空 loading,返回友好文案,不阻塞页面滚动、点击交互
  6. 组件生命周期资源释放

    • beforeDestroy钩子取消当前组件所有未完成任务
    • 提供terminateWorker方法,应用完全退出时彻底销毁线程释放内存
  7. 数据传输特性postMessage 基于结构化克隆算法,传输数据为独立副本,完全非引用传递,Worker 内部修改数据不会污染主线程原始数组。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询