开启 web 多线程——web worker

在之前写阅读器的时候有用到多线程:案例——《享阅·阅读器》解析。但是阅读器里我只开启了一个线程,所以并不能说真的使用了多线程。最近,在一个已经放弃的应用(能力所限,写不出来😂)里才尝试使用了多个线程。

基本的文件结构和阅读器里的差不多。首先还是安装 worker-loader 并配置。(框架是 vue)

module.exports = {
  chainWebpack: config => {
    config.module.rule('js').exclude.add(/\.worker\.js$/)
    config.module
      .rule('web-worker')
      .test(/\.worker\.js$/)
      .use('worker-loader')
      .loader('worker-loader')
      .end()
    config.output.globalObject('this')
  }
}

然后,utils 文件夹下面建立三个文件:utils.worker.js、promiseWorker.js、noDomUtils.js。

utils.worker.js 就是 worker 入口文件,会引入 worker 中需要用到的函数。

// utils.worker.js
import * as utils from '@/utils/noDomUtils'

// worker 收到信息并执行相关操作
onmessage = (e) => {
  const { action, param = [], timestamp, _sign } = e.data
  if (typeof utils[action] === 'function') {
    const res = {
      action,
      result: utils[action](...param),
      timestamp,
      _sign
    }
    postMessage(res)
  } else {
    console.log(`指定操作${action}不存在`)
  }
}

promiseWorker.js 是对 worker 通信的封装。相比于回调,promise 会更方便。多线程管理也在这里。

import Worker from '@/utils/utils.worker'
import { config } from '@/utils/setting'

const workerNum = config.threads // 线程数量
const quene = new Map() // 计算队列
const waiting = [] // 等待队列
const workers = new Array(workerNum).fill(null).map((_, index) => {
  return (
    {
      index,
      worker: new Worker(),
      idle: true, // 是否空闲
    }
  )
})

workers.map(item => {
  item.worker.addEventListener('message', e => {
    if (!e.data || !e.data._sign) {
      console.error('worker 返回数据错误')
      // quene.get(e.data._sign).reject('worker 返回数据错误')
    } else {
      quene.get(e.data._sign).resolve({
        result: e.data.result,
        e
      })
    }
    quene.delete(e.data._sign)
    item.idle = true
    // 尝试接受新任务
    assignJob()
  })
})


/**
 * 将等待队列中的任务加入空闲线程
 */
function assignJob() {
  let idleWorker = null
  let waitingJob = null
  if (waiting.length) {
    idleWorker = workers.find(item => item.idle)
    if (idleWorker) {
      idleWorker.idle = false
      waitingJob = waiting.shift()
      quene.set(waitingJob._sign, waitingJob.p)
      console.log(idleWorker.index)
      idleWorker.worker.postMessage({
        ...waitingJob.job,
        _sign: waitingJob._sign
      })
    }
  }
}

/**
 * @param job {
 *  action: '',
 *  param: {},
 *  timestamp // 可选
 * }
 */
export default (job) => {
  job.timestamp = job.timestamp || Date.now()
  return new Promise((resolve, reject) => {
    const _sign = Date.now() * Math.random()
    waiting.push({
      _sign,
      job,
      p: { resolve, reject }
    })
    // 分配线程
    assignJob()
  })
}

noDomUtils.js 里面是可以在 worker 里运行的函数。这里不贴了。

为了方便,我将 worker 挂在了 vue 原型上。类似代码如下:

// main.js
import Vue from 'vue'
import promiseWorker from '@/utils/promiseWorker'

Vue.prototype.worker = promiseWorker

因为 promiseWorker.js 无法得知哪些任务可以分割以及如何分割,所以业务代码自行分割任务。比如:

const res = await Promise.all(jobs.map(job => {
  return this.worker({
    action: 'xxx',
    param: [param1, param2]
  })
}))

web worker 赋予了 JavaScript 密集计算的能力。但是这个功能还是比较鸡肋。一般情况下,web 的瓶颈在网络,不在密集计算。并且,postMessage 传输时间可能会超过计算时间,特别是数据层级较多的时候。虽然可以使用可传输对象,但可传输对象类型有限制。除了图像、音频、视频处理和游戏等领域,其他场景很少会用到 web worker。