Worker补充内容
缺陷
- 同源策略
- 无法访问DOM 只能访问 navigator 和 locatin 对象
- 只能是js,不能是ts,不能用本地文件,不能执行alert,confirm
优点
- 多线程
- 适合执行CPU密集型,不阻塞主线程
步骤
- 获取指定文件
- 按照指定大小分blob片
对File调用 ``slice 方法:
slice方法用于从File对象中提取一个新的Blob或File对象,该对象表示原始文件的一部分。- 这个方法并不会读取文件的实际内容,只是创建一个新的
Blob或File对象,其中包含了指定范围内的数据。
-
分组/全部转换成 arraybuffer用于计算 分片hash
- 分片作用: 表示是否上传过
- 计算hash是cpu密集的,放入webworker
- 基于webWorker实现线程池加速计算Hash (md5 / crc32选择)
将blob转换为arrayBuffer的方法
使用blob.arrayBuffer() //使用blob对象本身的方法 这个方法是异步的!基于Promise
使用fileReader() fr.readAsArraybuffer() //使用了readFile API 基于事件API 所以如果使用这个,要利用一个异步函数封装一下,最后才能返回Promise.all
不放入webworker
如果你将这个过程放到 Worker 中, 由于 File 或 Blob 并不是 Worker 中的可 Transfer 对象
此处会导致 主线程与 Worker 通信时进行结构化克隆, 由此会产生额外的CPU性能消耗和内存消耗
而且如果文件很大时(大概超过2GB)会导致 Worker 线程 OOM (内存溢出错误)
实现webWorkerPool
WorkerPool 类有以下几个主要的属性和方法:
pool:这是一个WorkerWrapper对象的数组,每个WorkerWrapper对象都封装了一个 Web Worker 和它的状态。maxWorkerCount:这是 WorkerPool 可以同时运行的最大 Worker 数量。这个数量默认为设备的硬件并发级别,如果设备的硬件并发级别无法确定,则默认为 4。curRunningCount:这是一个 BehaviorSubject,表示当前正在运行的 Worker 数量。results:这是一个数组,用于存储每个 Worker 的结果。constructor(maxWorkers = navigator.hardwareConcurrency || 4):这是 WorkerPool 的构造函数,它接受一个可选的参数,表示最大 Worker 数量。exec<T>(params: ArrayBuffer[]):这是 WorkerPool 的主要方法,它接受一个 ArrayBuffer 数组作为参数,然后在 WorkerPool 中的每个 Worker 上并行执行任务。这个方法返回一个 Promise,当所有的 Worker 都完成任务时,这个 Promise 将解析为一个包含所有结果的数组。
在 exec 方法中,首先清空 results 数组,然后创建一个包含所有参数和它们的索引的数组。然后,创建一个新的 Promise,并在这个 Promise 的执行器函数中,订阅 curRunningCount BehaviorSubject。每当 curRunningCount 的值发生变化时,都会检查是否有空闲的 Worker 可以用来执行新的任务,如果有,则将任务分配给这些 Worker。当所有的任务都完成时,这个 Promise 将解析为 results 数组。
PromisePool
思路和workerPool大同小异
- 订阅一个并发数,回调为:并发数改变的时候如果还有任务需要执行,重新计算执行列表,从任务队列中取出
- 遍历执行列表执行传入的异步函数,promise 结果放入列表中,并更新并发数
- 当任务队列空,而且当前的并发数为0,说明没有任务需要并发,返回并发的结果
使用:
import { PromisePool } from './promisePool'
// 定义一些异步任务
const tasks = [
() => new Promise(resolve => setTimeout(() => resolve('Task 1'), 1000)),
() => new Promise(resolve => setTimeout(() => resolve('Task 2'), 2000)),
() => new Promise(resolve => setTimeout(() => resolve('Task 3'), 3000)),
]
// 创建一个 PromisePool 实例
const pool = new PromisePool(tasks, 2)
// 执行任务
pool.exec().then(results => {
console.log(results) // 输出: ['Task 1', 'Task 2', 'Task 3']
})
断点续传和秒传
- 后台维护一个接收到并确认的文件序列,如 [ 1,1,1,1,0,0,1,1 ] 1 表示对应的分片已经上载完成,0表示失败或者错误
- 再次上传的时候,通过整个文件的md5与后台通信获取文件序列,如果序列全1,则秒传成功
- 如果没有则返回全0 ,文件从头开始分片上传
- 否则上传序列为0的部分,等待后台校验返回
计算md5时优化内存
-
原本的方案是计算出所有分片的arrayBuffer,然后通过workerPool并发执行,这里可能会导致浏览器的内存占用严重
-
将blob分片数组分组,每一组是并发数量个,在执行之前才计算对应的arrayBuffer ,然后串行地执行一个每一个分组,每一个分组正好能够利用所有worker线程,
知识要点
-
File对象 继承 Blob 能够 slice
-
webworkerTransfer : 可转移对象通常用于共享资源,该资源一次仅能安全地暴露在一个 JavaScript 线程中。例如,
ArrayBuffer是一个拥有内存块的可转移对象。当此类缓冲区(buffer)在线程之间传输时,相关联的内存资源将从原始的缓冲区分离出来,并且附加到新线程创建的缓冲区对象中。原始线程中的缓冲区对象不再可用,因为它不再拥有属于自己的内存资源了。 -
读取大文件:使用FileReader的时候,利用readAsArrayBuffer能够读取大文件,但是如果调用readAsText浏览器会崩溃,因为一下把所有的内容读入内存,内存爆了。
- 加载到浏览器中的文件File类型是一个Blob类型,他是文件的元数据
- 通过readFileAPI我们才一次性或者分片的将文件读入
-
const fs = new FileReader() fs.onload()=>{} fs.readAsText()//一次性 fs.readAsArrayBuffer()//分片(大文件)
为什么用分片和前端hash
分片原因
- 大文件并发上传,提高速度
- 断点续传
- 基于真实上传进度的进度条
文件指纹原因
- 业务方要求
踩坑
WebWorker
-
TypeError: Failed to execute 'postMessage' on 'DedicatedWorkerGlobalScope': Overload resolution failed.
-
原因,webworker的postMessage方法调用不正确
-
心路历程 :根据教学文章,我将worker文件写为ts文件,在postMessage方法中,根据ts类型提示,我将他写为postMessage(data,'scope',[transferData]),我使用new Worker(new URL(string,baseURL),{type: 'module'})开启worker最后报错
-
解决方案:
- worker只能够加载js文件,不能加载ts文件
- postMessage的参数是 1 data(可以被结构化克隆的) ,2 使用transfer的数据
md5-single.worker?wo…file&type=classic:1 () Uncaught
-
SyntaxError: Unexpected token '<' (at md5-single.worker?wo…le&type=classic:1:1)
- 没有加载正确的worker文件