JS的多线程能力,WebWorker

637 阅读6分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

  1. 掌握基本的webworker创建
  2. 掌握worker的通信
  3. 实践一个简单的worker-loader

关于JS的各个Webworker全部说的话,其实内容挺多的,按红宝书说的,能出一本书,为此,这里约定,本文的Worker均指专有Worker。不涉及共享和服务类型的Worker

1. 基本背景

做前端的小伙伴都知道,js在浏览器端中一般是单线程的。于是在需求请求大量数据,处理和计算大量数据方面就是一个瓶颈,好在从 HTML5 规范开始,Web Worker 概念地引入为 JavaScript 引入了线程技术.

了解后台的同学千万不要认为Web Worker等同于后台意义的多线程,Web Worker现在有了多线程的形(有了多线程的用法),Web Worker的本质是支持把数据刷新与页面渲染两个动作拆开执行(不使用Web Worker的话这两个动作在主线程中是线性执行的)

一般用于:

  1. 触发长时间运行的脚本以处理计算密集型任务
  2. 高频的用户交互适用于根据用户的输入习惯、历史记录以及缓存等信息来协助用户完成输入的纠错、校正功能等类似场景,用户频繁输入的响应处理同样可以考虑放在web worker中执行

2. 引入Web Worker

相信看文章的你们一定都是优秀的vue开发者。为此这里使用vue3项目作为其背景

2.1 基本使用

public文件下创建worker.js

const xiaoMing = {
    name:'小明',
    age:18
}
// 发送线程消息
self.postMessage(xiaoMing); 
self.onmessage = e =>{
    console.log(e.data) // 打印 ['test','testone']
}

App.vue中创建一个worker如下:

setup(){
    const worker = new Worker('./work.js');
    worker.postMessage(['test','testone']);
    worker.onmessage = function(e){
        console.log(e.data) // 打印 {name: "小明", age: 18}
    }
}

值得的注意的是,在App.vue打印出来的对象不是worker.jsxiaoMing的引用,而是他的值的拷贝

2.2 消息发送接受,无识别标识

这个是什么意思呢? 改造一下worker.js

const xiaoMing = {
    name:'小明',
    age:18
}
// 发送线程消息
self.postMessage(xiaoMing); 
self.postMessage(Object.assign(xiaoMing,{name:'小红'}))
self.onmessage = e =>{
    console.log(e.data) // 打印 ['test','testone']
}

猜一猜App.vue中会怎么打印console.log(e.data),如果答打印{name: "小红", age: 18},其实就错了。实际上他会打印两次第一次是{name: "小明", age: 18},第二次是{name: "小明", age: 18},因为毕竟他发送了2次postMessage,所以会触发onmessage这个方法2次。

所以举一反三,worker给多次onmessage会怎么样呢?

没错,如果多加一个onmessage的话一起就会打印4次,所以这边可以得出一个结论,onmessage写一个就行了,发送消息的时候我们自定义一个标识符,然后根据这个标识符来进行相应的逻辑处理,也可以这么理解onmessage就相当于c++main{}

2.3 使用MessageChannel发送消息

  1. 创建一个信号通道const channel = new MessageChannel();
  2. 使用实例化worker对象的postMessage连接到worker (worker.postMessage({type:'map'},[channel.port1]))然后channel.port2.postMessage({type:'hhh'})去发送消息
  3. 在worker执行脚本中,接受消息,建立连接
    let messagePort = null;
    console.log('start');
    self.onmessage = e =>{
        console.log(self)
        if(!messagePort && e.ports[0]){
            messagePort = e.ports[0]
            // self.onmessage = null;
            messagePort.onmessage = (data)=>{
                console.log(data)
            };
        }
    };
    

2.4 使用BroadcastChannel

同源的脚本能够使用BroadcastChannel相互之间发送和接受消息。这种通道类型的设置比较简单。不需要像MessageChannel那样转移乱糟糟的接口。

App.vue代码如下:

const channel = new BroadcastChannel();
const worker = new Worker('./work.js');
channel.onmessage = (e)=>{
    console.log(e.data)
}
setTimeout(()=>{
    channel.postMessage({name:'小芳',age:24})
},1000)

worker.js代码如下:

const channel = new BroadcastChannel('workerChannel');
channel.onmessage = (event)=>{
    console.log(event.data)
    channel.postMessage('不点个赞?')
}

这里为什么需要一个定时器呢,因为这种信道没有端口所有权的概念。如果没有setTimeout(),又因为初始化,工作者线程的延迟,就会导致消息已经发送,但是工作者线程上的消息处理程序还没就位

2.5 什么是transferList

一个可选的Transferable对象的数组,用于传递所有权。如果一个对象的所有权被转移,在发送它的上下文中将变为不可用(中止),并且只有在它被发送到的worker中可用。

Transferable 接口代表一个能在不同可执行上下文之间,列如主线程和 Worker 之间,相互传递的对象。

实现了它的接口:ArrayBufferMessagePortImageBitmap OffscreenCanvas

为什么要提这个?

因为postMessage的第二可选参数就是transferList

3. 动态的Worker的创建

  1. 将运行语句写成字符串模式
  2. 使用Blob对象,(Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作)[developer.mozilla.org/zh-CN/docs/…]
  3. 使用URL.createObjectURL(blobjs),然后作为new Worker的参数即可
const response = `
    let messagePort = null;
    console.log('start');
    self.onmessage = e =>{
        console.log(self)
        if(!messagePort && e.ports[0]){
            messagePort = e.ports[0]
            // self.onmessage = null;
            messagePort.onmessage = (data)=>{
                console.log(data)
            };
        }
    };
`
const blobjs = new Blob([response],{type:"application/javascript"});
const worker = new Worker( URL.createObjectURL(blobjs))

不过这里值得注意的是:

调用 URL.createObjectURL 生成对象 URL,在创建完 Worker 后调用 URL.revokeObjectURL()让浏览器知道不再需要对这个文件保持引用。 URL.revokeObjectURL() 静态方法用来释放一个之前通过调用 URL.createObjectURL() 创建的已经存在的 URL 对象。当结束使用某个 URL 对象时,应该通过调用这个方法来让浏览器知道不再需要保持这个文件的引用了

4. 使用Worker进行项目优化例举

项目背景是使用MapBox加载瓦片地图。

  1. 后台返回数据量有2M这么大,要遍历每个数据添加标注,同时主进程要加载3d地图作为底图
  2. 将后台数据同时缓存至IndexedDB方便后续查询使用

所以为了项目更快一点就使用了worker,去请求处理后台返回的geojson数据,拿到数据后存入IndexedDB做数据缓存。然后使用mapbox的绘制功能,画出边界,和添加标注

使用这个主要目的就是为了减少获取数据和处理数据对主进程的影响,减少用户等待交互的时间

项目中,worker执行的代码放在public总感觉不是那么回事,因此需要一个worker-loader,可以直接去下载相关loader,也可以自己写一个

4.1 手写一个简单的worker-loader

思路很简单,大致就是这样:

  1. 使用函数的toString()
  2. 将toString的函数字符串和函数名执行串连接起来,使用new Blob的方式转换
  3. 然后使用URL.createObjectURL(blobjs),将结果输出即可。

一个简单的worker-loader就完成了