初步使用webworker

webworker是啥

基于多线程

javascript采用的是单线程模型,所有的任务都在一个主线程中完成,一次只能执行一个任务,如果有多个任务需要被执行的话,那么后面的任务会依次排队等着
浏览器创建一个后台线程来执行JavaScript代码。底层实现是通过JS文件创建一个Worker对象,不会影响主线程性能。

  • 需要线程通信进行数据传输。单向通信
  • Worker 线程有独立的内存空间,Message Queue,Event Loop,Call Stack 等, Worker 线程和主线程内存独立,可以处理indexDB
  • 处理大数据处理,例如图像处理、音频处理、视频编码、大文件传输等等。也可以用于实现实时通信、数据同步和数据可视化等等
  • Worker线程一旦创建成功了,就会始终运行了,不会被主线程的运行打断,虽然这样有利于随时响应主线程的通信,但是这也造成了Worker比较耗费资源,在我们编码过程中,可以适当的使用,并且如果使用完成后,我们应该需要关闭
  • 同源限制、文件限制,不能操作window\document,无法读取本地文件

数据传输问题

  • Structured Clone: 序列化和反序列化,很多对象直接丢失数据
  • Transfer Memory: 固定数据类型,移交使用权,移交之后,留下新对象
  • Shared Array Buffer: 共享数据,需要加锁,少数浏览器支持

借鉴 博客

问题: canvas获取base64导致block

demo代码如下

var context = canvas.getContext("2d")
image.onload = ()=> {
  canvas.width = image.width
  canvas.height = image.height
  this.image = image
  context.drawImage(image, 0, 0)
  setTimeout(() => {
    let time = Date.now()
    if (canvas.toBlob) {
      canvas.toBlob(function (blob) {
        var formData = new FormData()
        formData.append('file', blob, 'image.jpg')
        console.log("time1 = ", Date.now() - time) // 20ms
    // const reader = new FileReader();
    // reader.readAsDataURL(blob);
    // reader.onloadend = () => {
    //   reader.result
    //   console.log("time = ", Date.now() - time) // 20ms
    // }

    // time = Date.now() - time
    // console.log("time = ", time)
    // ...
  }, 'image/jpeg')
}
setTimeout(() => {
  time = Date.now()
  canvas.toDataURL('image/png')
  time = Date.now() - time
  console.log("toDataURL time = ", time) // 64
}, 3000);

}, 2000);
}

基本上 toBlob 的时间要比 toDataURL 要快 (image: 100 kB)

  • 如果要上传图片文件,FormData 基本上只要 20ms左右 即可
  • 如果要toBlob下转base64,则需要 20ms 左右, 产物 100 kB 左右
  • 如果需要同步处理,toDataURL 需要60ms左右, 产物 800 kB 左右
  • 时间因图片大小有差异

加大图片大小 (image: 4 MB)

  • 如果要上传图片文件,FormData 基本要 170ms左右 即可
  • 如果要toBlob下转base64,则需要 170ms 左右, 产物 1M 左右
  • 如果需要同步处理,toDataURL 需要 600ms左右, 产物 png(8M) jpeg(1M) jpg(8M)左右

浏览器有明显卡顿,如何优化?

webworker介入

demo

let time = Date.now()
let imageBitMap = await createImageBitmap(this.image) // image 对象
time = Date.now() - time
console.log("time transform = ", time)  // 100ms 
let time = Date.now()
let imageBitMap = await createImageBitmap(canvas)
console.log("time transform = ", Date.now() - time) // 极低,10ms以内

利用数据通信,ImageBitMap 可以移交数据权限,并image、canvas在主线程不受影响,即createImageBitmap实现数据copy、transform

worker.js

return new Promise((res, rej)=>{
  let imageBitMap = param.imageBitMap
  let offscreenCanvas = new OffscreenCanvas(imageBitMap.width, imageBitMap.height)
  let ctx = offscreenCanvas.getContext("2d");
  ctx.drawImage(imageBitMap, 0, 0)
  offscreenCanvas.convertToBlob({type: 'image/jpeg'}).then((blob) => {
    const reader = new FileReader();
    console.log("convertToBlob = ", blob)
    reader.readAsDataURL(blob);
    reader.onloadend = () => {
      param.imageBase64 = reader.result
      res(param);
    }
  })
})
  • 最终获得base64,可以转成数据权限 ArrayBuffer, 再移交到主线程。
  • 这地方transfer是移交对象组成的数组,移交之后准确的放置到对象的原相同位置,猜测是通过指针处理,又有沙箱处理,使用原地址不可访问;或者对原位置进行初始化原对象类型。
  • 再一个是会检测引用计数,或者智能指针使用记录,在获取该对象的引用过程中,出现被其他对象占有并不可释放,将会报错。需要深拷贝

多个webworker的通信

每一个worker都是一个线程,属于渲染进程(一个标签)的线程,worker之间可以使用MessageChannel进行通信:

// index.html

====worker1.js
// worker1.js
self.onmessage = function(e) {
  const  port = e.ports[0];
  port.postMessage("this is from worker1") //woker1中通过port1发送给port2        
}

====worker2.js
// worker2.js
self.onmessage = function(e) {
const port = e.ports[0];
port.onmessage = function(e) {
postMessage(e.data) //woker2中监听woker1中port1传递的数据,然后触发自身woker的监听函数
}
}

  • port1、port2只支持移交使用权一次,如果二次移交,就会报错
  • 管道通信,即先进先出消息缓冲区(内存),也叫信息消费
  • 管道通信是单向的,MessageChannel是双向的
  • onmessage默认start,addEventListener需要手动start
  • onmessage只有一个,addEventListener可以多个

结合wasm处理

在worker里引入wasm,并初始化

  • 使用import('path')遇到的问题
Failed to execute 'importScripts' on 'WorkerGlobalScope'

解决

A、大概率是设置了publicPath: './'
    修改为'/',或者不设置,默认就是'/'
    项目绕不开'./',这时候就要改为 require('path')
import最终在babel下会转为promise+require,常常作为异步加载来使用

通常不直接import { } from 'path',这个方式会直接解构wasm文件,并不会达到运行时加载wasm的效果,导致无法使用wasm

wasm例子: 伪代码

// wasm.js
xxxxxxx,xxx,ss;sss;ss;
// wasm生成,最后手动添加 export
export default Module;
// use.js
const cWasm = require('./wasm.js');
let cWasmModule = {};
function loadWasm() {
    // no-async-promise-executor
    return new Promise(async resolve=>{
        cWasmModule = await cWasm;
        cWasmModule.default.onRuntimeInitialized = () => {
            resolve();
        };
    })
}
export function buildxxxx() {
    return cWasmModule.default.buildFromC();
}
// worker.js
import { buildxxxx, loadWasm } from './use.js'

loadWasm()

self.addEventListener(‘message’, async (e) => { // 接收到消息
let data = e.data || {}
// console.log('message worker = ', data)
let res = {}
let list =
res.imageBitMap = await workerBuild()
if(res.imageBitMap){
list = [res.imageBitMap]
}
self.postMessage(res, list); // 向主线程发送消息
});

async function workerBuild(data={}){
let res = buildxxxx()
let imageBitMap = await createImageBitmap(canvas)
return imageBitMap
}


这是一个从 https://juejin.cn/post/7368770335226019875 下的原始话题分离的讨论话题