工作线程(Web Worker)允许开发人员编写能够脱离主线程、长时间运行而不被用户所中断的后台程序,去执行 计算密集型
或 高延迟
的事务或者逻辑,并同时保证页面对用户的及时响应
Web Worker
分为 专用线程 Dedicated Worker
和 共享线程 Shared Worker
浏览器作为宿主环境提供了多线程运行 JS
的能力,可通过并行的方式高效地提升执行效率
专用 worker
仅能被首次创建它的页面使用,本期我们将基于专用 Worker 来设计符合业务场景的优化方案
DedicatedWorker
简称 Worker
,其专用线程只能与一个页面渲染进程 Render Process
进行绑定和通信,不能多页面进行共享
主线程环境
-
创建一个专用worker
const worker = new Worker('hash.worker.js', { name: 'hash_worker' }) 复制代码
-
消息的发送和接收
worker.postMessage() worker.addEventListener('message', ({ data }) => { // event.data <=> data }) 复制代码
-
终止主线程
worker.terminate() 复制代码
注:立即终止,并不会等待工作线程去完成它剩余的操作
工作线程环境
- 消息的接收和发送
self.addEventListener('message', ({ data }) => { // event.data <=> data }) self.postMessage() 复制代码
- 关闭当前工作线程
self.close() 复制代码
- 加载外部脚本或第三方库
注:所引入的脚本与库都会绑定在子线程的全局对象上,即self.importScripts(urlA, urlB) 复制代码
self
或this
上
注意事项
-
同源策略的限制
Worker
线程中运行的脚本文件必须与主线程的脚本文件同源 -
操作 DOM 的限制
Worker
线程中无法读取主线程所在视图的DOM
对象,也无法使用window
和document
对象,但可以使用他们的子对象 -
本地文件的限制
Worker
线程中无法读取本地文件,因此它所加载的脚本必须来自于网络 -
消息通信
Worker
线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息的发送和接收来通信
场景示例
-
业务场景
由于业务需要,需要前端来计算选取文件的
MD5
值,然后传递给后端做业务逻辑的处理。受文件大小以及文件数量的影响,主线程会被阻塞,导致页面卡顿,甚至浏览器卡死由于
JS
单线程的特性,导致大量计算不仅会阻塞UI
的渲染,还无法充分发挥多核CPU
的计算能力 -
生活场景
单线程
超市排队结账的时候,当只有一个收银口的时候,所有人都需要排队依次结账。如果某个人买了很多的东西,收银员就得一件一件的进行价格扫描,相对来说要花费的时间就会长一点儿,那么其他的人都得在后边依次等着这个人结完,再依次进行结账,这样就会增加其他人的等候时间
多线程
如果现在收银口有多个或添加自助结账的通道,我们就可以到其他人少的地方去结账,这样可以更快的结账,避免等候时间太长
专用线程计算文件哈希值
-
工作线程(worker/hash.worker.js)
引用第三方脚本库并创建实例
importScripts(`${location.origin}/worker/lib/browser-md5-file.min.js`) const bmf = new self.browserMD5File() 复制代码
计算文件的哈希值
const computeHash = ({ file, data }) => { return new Promise((resolve, reject) => { bmf.md5(file, (err, md5) => { data.metamd5 = md5 err ? reject(err) : resolve({ file, data }) }, progress => { console.log('md5 progress number:', progress) } ) }) } 复制代码
接收文件相关信息进行处理并将结果返回给主线程
self.addEventListener('message', async ({ data }) => { try { const response = await Promise.all(data.map(item => computeHash(item))) self.postMessage(response) } catch(error) { console.error(error.message) } }) 复制代码
-
主线程封装(worker/md5.index.js)
const getHashByWorker = data => { return new Promise((resolve, reject) => { const worker = new Worker(`${location.origin}/worker/hash.worker.js`, { name: 'hash_worker' }) worker.postMessage(data) worker.addEventListener('message', ({ data }) => { worker.terminate() resolve(data) }) worker.addEventListener('error', err => reject(err)) }) } export default getHashByWorker 复制代码
-
主线程处理
import getHashByWorker from './worker' /* * {getHashByWorker} Function 获取文件的 MD5 值 * {upload} Function 上传文件 */ const [hashData, fileData] = await Promise.all([getHashByWorker, upload]) 复制代码
上传文件至文件服务器和计算文件的哈希值并行执行,全部执行完成后,将结果数据传输入库
性能分析
-
基于专用 Worker 的设计缺陷
通过
Promise.all()
并发获取文件的MD5
值。当上传的文件达到一定的量级的时候,此时内存的消耗会特别的大,不仅会出现内存溢出,还有可能会导致浏览器崩溃 -
浏览器崩溃
喔唷,崩溃啦!
错误代码:
Out of Memory
进一步的优化方案
- 全文件计算改为抽样计算
const computeHash = ({ file, data }) => { return new Promise((resolve, reject) => { bmf.md5(file.slice(0, 2 * 1024 * 1024), // 文件抽样 (err, md5) => { data.metamd5 = md5 err ? reject(err) : resolve({ file, data }) }, progress => { console.log('md5 progress number:', progress) } ) }) } 复制代码
- 优化工作线程,将并发改为队列
使用 第 03 期 - 对异步任务并发量进行限流 的方案实现 并发式依次队列,问题完美解决self.addEventListener('message', async ({ data }) => { try { const response = await asyncThrottling({ list: data, handler: computeHash }) self.postMessage(response) } catch(error) { console.error(error.message) } }) 复制代码
浏览器兼容性
-
在现代浏览器和移动端上的支持性相当的理想
专用 Worker 是最早实现并被最广泛支持的多线程方案
-
判断浏览器对专用
Worker
的支持性if (window.hasOwnProperty('Worker')) { console.log('支持,请放心使用!') } else { console.log('不支持,请优雅降级!') } 复制代码
-
一起交流学习
加群交流看沸点