Webpack中使用Web Worker
最近老大说计算消息的函数耗时很长,导致后续渲染直接卡住,让我调研下能否使用web worker来处理处理消息的函数看看是否能解决渲染阻塞的问题,然后我就开始动手实践。
什么是Web Worker
现代浏览器为JavaScript创造的多线程环境。可以新建并将部分任务分配到worker线程并行运行,两个线程可独立运行,互不干扰,可以通过自带的信息机制相互通信。
const worker = new Worker('work.js') //参数必须是网络获取的脚本文件,不能是本地资源
worker.postMessage('message from 主线程') //主线程向worker传递信息
worker.terminate() //主线程关闭worker
//worker中
self.postMessage('message from worker') //worker向主线程传递信息,self是worker中的全局对象
self.close() //worker线程关闭自己
限制:
- 同源限制
- 无法使用document/window/alert/confirm
- 无法加载本地资源
Web Worker的改造
Web Worker的交互类似与iframe之间的交互,因为postMessage
是异步任务,如果直接使用,没法确保信息回来的时机,因此我们可以对Web Worker进行一些处理,让其内部返回一个promise以达到返回数据时机可控,类似下方:
myWorker.postMessage('message').then(res => console.log(res))
然后去git上面找果然找到了想要的库 promise-worker,看了下其内部的实现,大致分为两个文件,一个是主文件中创建的,一个是worker文件中使用的,因为设计场景很少,所以就稍作修改,然后就是我自己的了
主文件中创建Worker
let messageIds = 0 // 为了区分每一条消息 因此需要一个id
export class PromiseWorker {
constructor(worker) {
this.worker = worker
this.callbacks = {}
}
postMessage(userMessage) {
let messageId = messageIds++
let messageToSend = [messageId, userMessage]
// 通过返回一个promise达成可以使用.then来获取返回数据的能力
return new Promise((resolve, reject) => {
// 每次都创建一个新messageId的回调储存在内部,内部传入promise的reject resolve方便调用回调之后直接返回数据给外部
this.callbacks[messageId] = (error, result) => {
if (error) {
reject(new Error(error.message))
}
resolve(result)
}
// 以下是创建一个传递消息的通道
// 因为在实际使用中 发现window.addEventListener('message') 监听不到 又因为worker中也支持channel 所以直接使用了MessageChannel
// MessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性发送数据
const channel = new MessageChannel()
// 外部使用port1监听worker发回来的信息
channel.port1.onmessage = e => {
onMessage(this, e)
}
// worker.postMessage(aMessage, transferList)
// 第二个参数transferList类型 就是一个Transferable对象的数组,用于传递所有权
// MessagePort的实例对象 就是Transferable对象 因此我们就可以将port2发送给worker内部用来传递消息
this.worker.postMessage(messageToSend, [channel.port2])
})
}
}
// 外部用来处理worker返回消息的函数
function onMessage(self, e) {
let message = e.data
if (!Array.isArray(message) || message.length < 2) return
let [messageId, error, result] = message
// 通过data中的messageId找到对应的回调函数
let callback = self.callbacks[messageId]
if (!callback) return
delete self.callbacks[messageId]
// 执行回调函数 并且传入worker内部返回的error以及result
callback(error, result)
}
Worker文件内部处理函数
// worker内部的注册函数传入一个函数callback,它就是用来处理大数据的函数
export function registerPromiseWorker(callback) {
// 最终发送消息的地方 e.ports[0]就是主线程发送过来的port2 通过其上面的postMessage把消息发送给port1
function postOutgoingMessage(e, messageId, error, result) {
// 如果有错误就在发送一个[messageId, error]
if (error) {
e.ports[0].postMessage([
messageId,
{
message: error.message
}
])
} else {
// 否则就发送[messageId, null, result]
e.ports[0].postMessage([messageId, null, result])
}
}
function tryCatchFunc(callback, message) {
try {
return { res: callback(message) }
} catch (e) {
return { err: e }
}
}
// 处理主线程发送的数据
function handleIncomingMessage(e, callback, messageId, message) {
// 捕获可能出现的错误
let result = tryCatchFunc(callback, message)
if (result.err) {
postOutgoingMessage(e, messageId, result.err)
} else if (!isPromise(result.res)) { // 如果callback不是promise直接发送处理后的结果
postOutgoingMessage(e, messageId, null, result.res)
} else {
// 如果callback是promise则通过then去处理两种情况fulfillment rejection
result.res.then(
finalResult => {
postOutgoingMessage(e, messageId, null, finalResult)
},
finalError => {
postOutgoingMessage(e, messageId, finalError)
}
)
}
}
// 处理主线程发过来的函数
function onIncomingMessage(e) {
let payload = e.data
if (!Array.isArray(payload) || payload.length !== 2) return
let [messageId, message] = payload
// 判断callback是否可以执行 如果不可以则直接返回错误 否则就去处理message中的信息
if (typeof callback !== 'function') {
postOutgoingMessage(e, messageId, new Error('Please pass a function init register().'))
} else {
handleIncomingMessage(e, callback, messageId, message)
}
}
// worker内部通过self.addEventListener('message')去监听主线程发过来的消息进行处理
self.addEventListener('message', onIncomingMessage)
}
最终的使用
由于Worker不能使用本地文件,所以我们直接new Worker('./test.worker.js')
是不行的,因此需要稍作处理,在webpack中使用需要加一个worker-loader
去处理worker.js文件
// 首先安装一下worker-loader
npm i worker-loader -D
// 另外有一个坑点 worker-loader在2.0.0之后不再支持webpack3 因此如果webpack3需要安装1版本
npm i worker-loader@1.1.1 -D
// 安装好之后在webpack.config.js中配置
module: {
// ...
rules: [
// ...
{
test: /.worker.js$/, // 匹配所有的xxx.worker.js
loader: 'worker-loader'
}
// ...
]
// ...
}
// test.worker.js
import { registerPromiseWorker } from '../webworker/index'
registerPromiseWorker(m => {
return `${m} pong`
})
// index.js webpack5以下
import Worker from './test.worker.js' // loader处理过后可以直接import引入
import { PromiseWorker } from '../webworker/index'
const worker = new Worker() // 直接创建实例
const pw = new PromiseWorker(worker)
pw.postMessage('ping').then(result => {
console.log('返回内容', result)
})
// index.js webpack5之后可以不需要使用worker-loader 直接生成一个url文件地址引入
const workerURL = new URL('./test.worker.js', import.meta.url)
const worker = new Worker(workerURL)
const pw = new PromiseWorker(worker)
pw.postMessage('ping').then(result => {
console.log('返回内容', result)
})
转载自:https://juejin.cn/post/6996334834662506503