Vue2 数万级设备数据全局单例 WebWorker 完整实现
项目结构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 {ArrayObject} rawList 原始设备数组含循环引用/冗余属性 * returns {ArrayObject} 可安全跨线程传输的纯数据 */ 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 {PromiseObject} {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 classdevice-page div classfilter-bar select v-modelfilterParams.status changehandleSearch option :valueundefined全部状态/option option :value0正常/option option :value1故障/option /select select v-modelfilterParams.online changehandleSearch option :valueundefined全部在线/option option :valuetrue在线/option option :valuefalse离线/option /select button clickhandleSearch :disabledloading查询设备数据/button /div div v-ifloading数据运算中请稍候.../div div v-iferrMsg stylecolor:red{{ errMsg }}/div !-- 聚合统计展示 -- div v-ifstatData p设备总数{{ statData.totalDevice }}/p p在线{{ statData.onlineCount }}台 / 离线{{ statData.offlineCount }}台/p p平均温度{{ statData.avgTemperature }}℃/p /div !-- 格式化后设备表格 -- table v-iftableList.length tr v-foritem in tableList :keyitem.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-loadervue-cli 项目安装依赖npm install worker-loader -Dvue.config.js 添加 loader 匹配 worker 文件module.exports { configureWebpack: { module: { rules: [ { test: /\.worker\.js$/, use: { loader: worker-loader } } ] } } }关键需求点全覆盖说明全局单例 Worker采用单例模式全局仅创建 1 个后台线程所有页面组件共用避免浏览器创建过多线程耗尽资源。taskId 任务隔离、丢弃过期结果每个运算生成自增唯一 taskId绑定组件 compUid 区分多页面并发发起新筛选时更新组件最新 taskId旧任务返回直接丢弃不更新 UI切换筛选条件主动取消未完成旧计算减少无效后台运算结构化克隆兼容处理循环引用报错解决cleanDeviceData预处理WeakMap 标记循环对象、剔除嵌套循环引用仅保留业务核心字段过滤冗余属性 / 函数 / DOM大幅减小传输数据体积捕获messageerror监听 DataCloneError 序列化失败给用户提示三大耗时逻辑全部移入 Worker批量多条件过滤状态、在线、区域、设备类型多字段联合筛选数据格式化空值清洗、状态码转中文、时间格式化、电压 / 功率单位换算聚合统计设备总数、在线离线分组、设备类型分组计数、温度电压平均值完整异常捕获体系worker.onerror捕获 Worker 内部代码计算报错messageerror捕获跨线程数据序列化失败循环引用 / 非法对象postMessage 外层 try-catch捕获发送消息阶段序列化异常统一清空 loading返回友好文案不阻塞页面滚动、点击交互组件生命周期资源释放beforeDestroy钩子取消当前组件所有未完成任务提供terminateWorker方法应用完全退出时彻底销毁线程释放内存数据传输特性postMessage 基于结构化克隆算法传输数据为独立副本完全非引用传递Worker 内部修改数据不会污染主线程原始数组。