分片计算文件MD5,并借助WebWorker和队列优化

之前也有做过计算文件MD5做相关校验,最近业务上再次遇上,同时需要大量文件同时计算,难免需要优化相关性能。这里用到分片计算,同时将计算过程放到webworker,避免长时间占用主线程造成卡顿,并且通过队列控制并发,避免占用过多计算机资源,造成浏览器卡顿。

整个文件直接计算MD5

javascript 复制代码
import SparkMD5 from 'spark-md5'
const spark = new SparkMD5.ArrayBuffer() 
export const createMD5 = file => {
    // 验证file
    return new Promise((resolve,reject)=>{
        const fileReader = new FileReader()
         fileReader.readAsArrayBuffer(file)
         fileReader.onerror = e => {
             console.error(e)
             return reject(e)
             }
        fileReader.onload = () => {
            try{
            resolve(spark.hash(dataBuffer)) 
            }catch(e){
                 console.error(e)
             return reject(e)
            }
            
         }
    })
    
}

直接加载整个文件,将整个文件的二进制数据arrayBuffer丢给计算工具计算MD5,如果是一个小文件就还好,但是大文件,批量的文件,一下子将大量资源放到内存里会造成明显的卡顿 .

分片计算MD5

spark-md5推荐:Incremental md5 performs a lot better for hashing large amounts of data, such as files. One could read files in chunks, using the FileReader & Blob's, and append each chunk for md5 hashing while keeping memory usage low.

增量 md5在散列大量数据(比如文件)时表现得更好。可以使用 FileReader & Blob 的块来读取文件,并为 md5哈希添加每个块,同时保持较低的内存使用。

通过File.slice将文件分片,一块一块地增量处理spark.append(chunkArrayBuffer),最后通过spark.end()获取最终的文件md5,可以保持低内存使用,当然,我们平时讲的文件分片上传等也是基于File.slice。

上代码

javascript 复制代码
import SparkMD5 from 'spark-md5'
/*
 * @description 创建文件的MD5值,根据文件大小动态选择分片大小和快速循环的方式
 * @param {Object} file el文件对象或文件对象
 * @return {String} MD5 值
 * */
const maxChunkSize = 1024 * 1024 // 每次处理数据块的最大值,1MB
const chunksPerCycle = 100 // 每个计算周期中处理的数据块数
export const createMD5 = file => {
  file = file.raw || file
  let stamp = Date.now()
  console.time('md计算耗时' + stamp)
  return new Promise((resolve, reject) => {
    // 分片放worker处理
    const fileReader = new FileReader()
    try {
      let currentChunk = 0
      const totalChunks = Math.ceil(file.size / maxChunkSize)
      const spark = new SparkMD5.ArrayBuffer()
      // 处理数据块
      const processChunk = start => {
        try {
          const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
          const end = Math.min(start + maxChunkSize, file.size)
          const chunk = blobSlice.call(file, start, end)
          fileReader.readAsArrayBuffer(chunk)
        } catch (e) {
          console.error(e)
        }
      }
      // 文件读取成功
      fileReader.onload = () => {
       spark.append(fileReader.result) // 将当前数据块内容追加到 MD5 计算器
        currentChunk += 1
        if (currentChunk >= totalChunks) {
          const md5Hash = spark.end() // 完成 MD5 计算
           spark.destroy() // 销毁计算器
           fileReader.abort() //释放资源
           console.timeEnd('计算MD5')
           return resolve(md5Hash)
        } else if (currentChunk % chunksPerCycle === 0) {
          // 在处理指定数量的数据块后,设置一个任务延迟以使 UI 线程有空间处理
          setTimeout(() => {
            processChunk(currentChunk * maxChunkSize)
          }, 0)
        } else {
          // 继续处理下一个数据块
          processChunk(currentChunk * maxChunkSize)
        }
      }
      fileReader.onerror = e => {
        console.error(e)
        return reject(e)
      }
      // 开始处理第一个数据块
      processChunk(0)
    } catch (e) {
      fileReader.abort()
      reject(e)
    } finally {
    }
  })
}

借助WebWorker优化

md5计算属于高密度计算,如果直接放在js主线程,长时间占用造成卡顿难免对用户体验不好,这里也可以发挥出webworker的优势,本来webworker就是为了线程可以执行任务而不干扰用户界面而生。

这里也有两种方式,

第一种,将File直接传给worker,在worker里面分片读取计算,但是这样的话,因为File对象这种二进制数据直接传递给worker,浏览器会先给FIle做一层拷贝,但是拷贝方式多少会造成性能问题。

当然,二进制数据可以通过Transferable Objects,也就是转移数据的方法,主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了(防止出现多个线程同时修改数据的局面)。那么我们能不能将File通过这种方式传过去呢?不行,Transferable Objects只支持以下几个类型,就算支持将File转过去,我们还得想一个问题就是,将FIle转过去,主线程就不能用这个File对象了,那你后续怎么做进一步的处理,上传啥的。

以下是可以被转移的不同规范的对象:

第二种,反正也是切片,我这里就直接在主线程切片读取,将读取后的chunkBuffer通过Transferable Objects传递到worker线程。

worker代码:

php 复制代码
import SparkMD5 from 'spark-md5'
const spark = new SparkMD5.ArrayBuffer()
self.addEventListener('message', ({ data }) => {
  const { dataBuffer, status } = data
  try {
    if (status === 'ing') {
      spark.append(dataBuffer)
    } else if (status === 'end') {
      self.postMessage({ md5: spark.end(), status: 'success' })
      spark.destroy() // 销毁计算器
    }
  } catch (e) {
    self.postMessage({ status: 'error', error: e })
    console.error(e)
  }
})

主线程:

javascript 复制代码
/*
 * @description 创建文件的MD5值,根据文件大小动态选择分片大小和快速循环的方式
 * @param {Object} file el文件对象或文件对象
 * @return {String} MD5 值
 * */
const maxChunkSize = 1024 * 1024 // 每次处理数据块的最大值,1MB
const chunksPerCycle = 100 // 每个计算周期中处理的数据块数
export const createMD5 = file => {
  file = file.raw || file
  let stamp = Date.now()
  console.time('md计算耗时' + stamp)
  let worker = new Worker(
    /* webpackChunkName: "md5-encode.worker" */ new URL('../worker/md5-encode.worker.js', import.meta.url)
  )
  
  return new Promise((resolve, reject) => {
    // 分片放worker处理
    const fileReader = new FileReader()
    try {
      let currentChunk = 0
      const totalChunks = Math.ceil(file.size / maxChunkSize)
      // 处理数据块
      const processChunk = start => {
        try {
          let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
          const end = Math.min(start + maxChunkSize, file.size)
          const chunk = blobSlice.call(file, start, end)
          fileReader.readAsArrayBuffer(chunk)
        } catch (e) {
          console.error(e)
        }
      }
      // 文件读取成功
      fileReader.onload = () => {
        worker.postMessage({ dataBuffer: fileReader.result, status: 'ing' }, [fileReader.result])
        currentChunk += 1
        if (currentChunk >= totalChunks) {
          worker.postMessage({ status: 'end' })
          fileReader.abort()
          // console.timeEnd('计算MD5')
          return
          // return resolve(md5Hash)
        } else if (currentChunk % chunksPerCycle === 0) {
          // 在处理指定数量的数据块后,设置一个任务延迟以使 UI 线程有空间处理
          setTimeout(() => {
            processChunk(currentChunk * maxChunkSize)
          }, 0)
        } else {
          // 继续处理下一个数据块
          processChunk(currentChunk * maxChunkSize)
        }
      }
​
      fileReader.onerror = e => {
        console.error(e)
        return reject(e)
      }
       worker.onmessage = ({ data }) => {
        const { md5, status, error } = data
        if (status === 'success') {
          resolve(md5)
        } else {
          reject(error)
        }
        console.timeEnd('md计算耗时' + stamp)
        worker.terminate()
      }
      // 开始处理第一个数据块
      processChunk(0)
    } catch (e) {
      fileReader.abort()
      reject(e)
    } finally {
    }
  })
}

第二种方式地第一种直接传File对象会快出File拷贝的时间,处理一个300多m的文件,整体会快几百ms到1s。

借助队列函数进一步优化

现在计算md5不会堵塞主线程了,但是如果大量文件同时进行分片读取,同样会造成一定的卡顿,同时,大家也要知道,worker开到一定数量也是会造成浏览器卡顿的,和电脑的资源大小有关,所以可以通过队列控制计算md5方法的并发量,避免一下子占用太多资源。

以下是我写的一个队列函数,结合一下上面的createMD5即可达到目的。

javascript 复制代码
​
/**
 * 队列操作
 * @param concurrency 同时执行的数量
 * @param fn 操作函数 异步函数
 * @param fn.dataItem 操作的数据
 * @param fn.getRemoveQueue 撤销排队中的某一个任务
 * @returns {function(*=, *=): Promise<unknown>}
 * 使用方法
 * let removeFn = null
 * const getRemoveFn = fn => removeFn = fn
 * const handleDataByQueue = createQueue(3, async (dataItem) => {
 *  await sleep(1000)
 *  console.log(dataItem)
 *  return dataItem
 *  })
 *  handleDataByQueue(1, getRemoveFn)
 *  // 取消排队
 *  setTimeout(() => {
 *  removeFn && removeFn()
 *  }, 1000)
 * */
export const createQueue = (concurrency, fn) => {
  const queue = []
  const runningQueue = []
  // 撤销排队中的某一个任务
​
  const removeQueue = task => {
    const index = queue.findIndex(item => item === task)
    if (index !== -1) {
      console.log('取消排队')
      queue.splice(index, 1)
    }
  }
  const process = (dataItem, getRemoveQueueSource) => {
    return new Promise((resolve, reject) => {
      const run = async () => {
        if (runningQueue.length >= concurrency) {
          queue.push(run)
          getRemoveQueueSource && getRemoveQueueSource(() => removeQueue(run))
          return
        }
        runningQueue.push(run)
        try {
          const result = await fn(dataItem)
          resolve(result)
        } catch (e) {
          reject(e)
        } finally {
          runningQueue.splice(runningQueue.indexOf(run), 1)
          if (queue.length) {
            queue.shift()()
          }
        }
      }
      run()
    })
  }
  return process
}

使用:

scss 复制代码
// 10个并发量
const createMD5ByQueue = createQueue(10, createMD5)
​
// createMD5ByQueue(file1)
// createMD5ByQueue(file2)
// createMD5ByQueue(file3)
// createMD5ByQueue(file4)

如有不妥,多多指教!

相关推荐
王哈哈^_^1 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie1 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic2 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿2 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具3 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
清灵xmf3 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据3 小时前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_390161773 小时前
防抖函数--应用场景及示例
前端·javascript
334554324 小时前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test4 小时前
js下载excel示例demo
前端·javascript·excel