WebWorker 知识点
Web Worker
分为 Dedicated Web Worker
和 Shared Web Worker
两类.
Dedicated Web Worker
仅为创建它的 JSVM 进程服务,当其所属的 JSVM 进程结束该Dedicated Web Worker
线程也将结束;Shared Web Worker
为创建它的 JSVM 进程所属页面的域名服务,当该域名下的所有 JSVM 进程均结束时该Shared Web Worker
线程才会结束;
怎么使用 web worker
主线程使用 new 命令调用 Worker() 构造函数创建一个 Worker 线程
var worker = new Worker('xxxxx.js')
:Worker 构造函数接收参数为脚本文件路径
主线成指定监听函数监听 Worker 线程的返回消息
worker.onmessage = function (event) {console.log(event.data)}
:data
为 Worker 发来的数据
由于主线程与 Worker 线程存在通信限制,不再同一个上下文中,所以只能通过消息完成
worker.postMessage("hello world")
worker.postMessage({action: "ajax", url: "xxxxx", method: "post"})
当使用完成后,如果不需要再使用可以在主线程中关闭 Worker
worker.terminate()
:Worker 也可以关闭自身,在 Worker 的脚本中执行self.close()
错误的处理,主线程可以监听 Worker 是否错误,如果有错误则会触发主线程的 error 事件
worker.onerror(function (evet) { // ... })
在 Worker 中使用其他脚本
如果需要引用其他脚本可以使用 importScripts
importScripts('scripts1.js')
:该方法可以同时加载多个脚本importScripts('scripts1.js','scripts2.js')
数据通信
主线程与 Worker 之间通信时拷贝的方式进行,即是传值而不是传址。Worker 中对通信数据的修改并不会影响到主线程。
事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原
但是拷贝的形式做数据传输会造成性能问题,比如主线程向 Worker 发送几百 MB 的数据,默认情况下,浏览器会生成一份拷贝。为了解决这个问题,JAVASCRIPT 允许主线程将二进制数据直接转移给 Worker,但是转移控制权后,主线程就不再能使用这些数据。这是为了防止多个线程同时修改数据的情况发生。
这种转移数据控制权的方法叫做 Transferable Objects 这使得主线程可以快速的把数据移交给子线程。不会产生性能负担
interface Worker extends EventTarget, AbstractWorker {
onmessage: ((this: Worker, ev: MessageEvent) => any) | null;
onmessageerror: ((this: Worker, ev: MessageEvent) => any) | null;
postMessage(message: any, transfer: Transferable[]): void;
postMessage(message: any, options?: StructuredSerializeOptions): void;
terminate(): void;
addEventListener<K extends keyof WorkerEventMap>(type: K, listener: (this: Worker, ev: WorkerEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof WorkerEventMap>(type: K, listener: (this: Worker, ev: WorkerEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
web worker
能力边界
在使用 Web Worker
前我们要了解它的能力边界
同源限制
- 以
http(s)://
协议加载给Web Worker
线程运行的脚本时,其URL
必须和UI 线程所属页面的URL
同源; - 不能加载客户端本地脚本给 WebWorker 线程运行(即采用
file://
协议),即使 UI 线程所属页面也是本地页面;
DOM 和 BOM 限制
- 无法访问 UI 线程所属页面的任何 DOM 元素;
- 可访问如下 BOM 元素:
XMLHttpRequest/fetch
setTimeout/clearTimeout
setInterval/clearInterval
location
注意该 location 指向的是 WebWorker 创建时以 UI 线程所属页面的当前 Location 为基础创建的 WorkerLocation 对象,即使此后页面被多次重定向,该 location 的信息依然保持不变。 1.2.5. navigator,注意该 navigator 指向的是 WebWorker 创建时以 UI 线程所属页面的当前 Navigator 为基础创建
navigator
注意该 navigator 指向的是 WebWorker 创建时以 UI 线程所属页面的当前 Navigator 为基础创建的 WorkerNavigator 对象,即使此后页面被多次重定向,该 navigator 的信息依然保持不变
通信限制
UI 线程和 Web Worker
线程间必须通过消息机制进行通信。
一、 Dedicated Web Worker
详解
1. 基本使用
UI 线程
const worker = new Worker('work.js') // 若下载失败如 404,则会默默地失败不会抛异常,即无法通过 try/catch 捕获。
const workerWithName = new Worker('work.js', {
name: 'worker2'
}) // 为 Worker 线程命名,那么在 Worker 线程内的代码可通过 self.name 获取该名称。
worker.postMessage('Send message to worker.') // 发送文本消息
worker.postMessage({
type: 'message',
payload: ['hi']
}) // 发送 JavaScript 对象,会先执行序列化为 JSON 文本消息再发送,然后在接收端自动反序列化为 JavaScript 对象。
const uInt8Array = new Uint8Array(new ArrayBuffer(10))
for (let i = 0; i < uint8array.length; ++i) {
uInt8Array[i] = i * 2
}
worker.postMessage(uInt8Array) // 以先序列化后反序列化的方式发送二进制数据,发送后主线程仍然能访问 uInt8Array 变量的数据,但会造成性能问题。
worker.postMessage(uInt8Array, [uInt8Array]) // 以 Transferable Objets 的方式发送二进制数据,发送后主线程无法访问 uInt8Array 变量的数据,但不会造成性能问题,适合用于影像、声音和 3D 等大文件运算。
// 接收 worker 线程向主线程发送的消息
worker.onmessage = event => {
console.log(event.data)
}
worker.addEventListener('message', event => {
console.log(event.data)
})
// 当发送的消息序列化失败时触发该事件。
worker.onmessageerror = error => console.error(error)
// 捕获 Worker 线程发生的异常
worker.onerror = error => {
console.error(error)
}
// 关闭 worker 线程
worker.terminate()
Worker 线程
// Worker 线程的全局对象为 WorkerGlobalScrip,通过 self 或 this 引用。调用全局对象的属性和方法时可以省略全局对象。
// 接收主线程向 worker 线程发送的消息
self.addEventListener('message', event => {
console.log(event.data)
})
addEventListener('message', event => {
console.log(event.data)
})
this.onmessage = event => {
console.log(event.data)
}
// 当发送的消息序列化失败时触发该事件。
self.onmessageerror = error => console.error(error)
// 向主线程发送消息
self.postMessage('send text to main worker')
// 结束自身所在的 Worker 线程
self.close()
// 导入其他脚本到当前的 Worker 线程,不要求所引用的脚本必须同域。
self.importScripts('script1.js', 'script2.js')
2. 通过 Web Worker
运行本页脚本
方式 1: Blob
和 URL.createObjectURL
限制:UI 线程所属页面不是本地页面,即必须为 http(s)://
协议。
const script = `addEventListener('message', event => {
console.log(event.data)
postMessage('echo')
}`
const blob = new Blob([script])
const url = URL.createObjectURL(blob)
const worker = new Worker(url)
worker.onmessage = event => console.log(event.data)
worker.postMessage('main thread')
setTimeout(() => {
worker.terminate()
URL.revokeObjectURL(url) // 必须手动释放资源,否则需要刷新 Browser Context 时才会被释放。
}, 1000)
方式 2: Data URL
限制:无法利用 JavaScript 的 ASI 机制少写分号。
优点:即使 UI 线程所属页面是本地页面也可以执行
// 由于 Data URL 的内容为必须压缩为一行,因此 JavaScript 无法利用换行符达到分号的效果。
const script = `addEventListener('message', event => {
console.log(event.data);
postMessage('echo');
}`
const worker = new Worker(`data:,${script}`)
// 或 const worker = new Worker(`data:application/javascript,${script}`)
worker.onmessage = event => console.log(event.data)
worker.postMessage('main thread')
二、 Shared Web Worker
详解
共享线程可以和多个同域页面间通信,当所有相关页面都关闭时共享线程才会被释放。
这里的多个同域页面包括:
- iframe 之间
- 浏览器标签页之间
简单示例
UI 主线程
const worker = new SharedWorker('./worker.js')
worker.port.addEventListener('message', e => {
console.log(e.data)
}, false)
worker.port.start() // 连接 worker 线程
worker.port.postMessage('hi')
setTimeout(() => {
worker.port.close() // 关闭连接
}, 10000)
Shared Web Worker
线程
let conns = 0
// 当 UI 线程执行 worker.port.start() 时触发建立连接
self.addEventListener('connect', e => {
const port = e.ports[0]
conns += 1
port.addEventListener('message', e => {
console.log(e.data) // 注意 console 对象指向第一个创建 Worker 线程的 UI 线程的 console 对象。即如果 A 先创建 Worker 线程,那么后续 B、C 等 UI 线程执行 worker.port.postMessage 时回显信心依然会发送给 A 页面。
})
// 建立双向连接,可相互通信
port.start()
port.postMessage('hey')
})
示例——广播
UI 主线程
const worker = new SharedWorker('./worker.js')
worker.port.addEventListener('message', e => {
console.log('SUM:', e.data)
}, false)
worker.port.start() // 连接 worker 线程
const button = document.createElement('button')
button.textContent = 'Increment'
button.onclick = () => worker.port.postMessage(1)
document.body.appendChild(button)
Shared Web Worker
线程
let sum = 0
const conns = []
self.addEventListener('connect', e => {
const port = e.ports[0]
conns.push(port)
port.addEventListener('message', e => {
sum += e.data
conns.forEach(conn => conn.postMessage(sum))
})
port.start()
})
工程化
通过 Webpack 的 worker-loader 打包代码
web worker
实际项目中应该怎么使用呢?或者说如何更好的集成到工程自动化工具——Webpack 呢? worker-loader
和 shared-worker-loader
就是我们想要的。 通过 worker-loader
将代码转换为 Blob 类型,并通过 URL.createObjectURL
创建 url
分配给 WebWorker 线程执行。
安装
npm install worker-loader -D
配置 Webpack.config.js
// 处理 worker 代码的 loader 必须位于 js 和 ts 之前
{
test: /\.worker\.ts$/,
use: {
loader: 'worker-loader',
options: {
name: '[name]:[hash:8].js', // 打包后的 chunk 的名称
inline: true // 开启内联模式,将 chunk 的内容转换为 Blob 对象内嵌到代码中。
}
}
}, {
test: /\.js$/,
use: {
loader: 'babel-loader'
},
exclude: [path.resolve(__dirname, 'node_modules')]
}, {
test: /\.ts(x?)$/,
use: [{
loader: 'babel-loader'
},
{
loader: 'ts-loader'
} // loader 顺序从后往前执行
],
exclude: [path.resolve(__dirname, 'node_modules')]
}
UI 线程代码
import MyWorker from './my.worker'
const worker = new MyWorker('');
worker.postMessage('hi')
worker.addEventListener('message', event => console.log(event.data))
Worker 线程代码
const worker: Worker = self as any
worker.addEventListener('message', event => console.log(event.data))
export default null as any // 标识当前为 TS 模块,避免报 xxx.ts is not a module 的异常
RPC 类库 Comlink
一般场景下我们会这样使用 Web Worker
,
- UI 线程传递参数并调用运算函数;
- 在不影响用户界面响应的前提下等待函数返回值;
- 获取函数返回值继续后续代码。
翻译为代码就是
let arg1 = getArg1()
let arg2 = getArg2()
const result = await performCalcuation(arg1, arg2)
doSomething(result)
而 UI 线程和 Web Worker
线程的消息机制通信机制显然会加大代码复杂度,而 Comlink
类库恰好能抚平这道伤疤
UI 线程
import * as Comlink from 'comlink'
async function init() {
const cl = Comlink.wrap(new Worker('worker.js'))
console.log(`Counter: ${await cl.counter}`)
await cl.inc()
console.log(`Counter: ${await cl.counter}`)
}
Worker 线程
import * as Comlink from 'comlink'
const obj = {
counter: 0,
inc() {
this.counter += 1
}
}
Comlink.expose(obj)
Electron 中使用 WebWorker
Electron 中使用 Web Worker 的同源限制中开了个口——UI 线程所属页面 URL 为本地文件时,所分配给 Web Worker 的脚本可为本地脚本。
其实 Electron 打包后读取的 HTML 页面、脚本等都是本地文件,如果不能分配本地脚本给 Web Worker 执行,那就进入死胡同了。
const path = window.require('path')
const worker = new Worker(path.resolve(__dirname, 'worker.js'))
上述代码仅表示 Electron 可以分配本地脚本给 WebWorker 线程执行,但实际开发阶段一般是通过 http(s)://
协议加载页面资源,而发布时才会打包为本地资源。
所以这里还要分为开发阶段用和发布用代码,还涉及资源的路径问题,所以还不如直接转换为 Blob 数据内嵌到 UI 线程的代码中更便捷。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 小程序 凹凸圆选中动画 tabbar
下一篇: 彻底找到 Tomcat 启动速度慢的元凶
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论