useWebWorkerFn() composable 会在一个 Web Worker 中运行你的某个函数,把它放到主线程之外执行,并以 Promise 的形式把结果交给你。你无需编写任何 worker 文件:函数本身就成为了 worker 脚本。它适用于那些原本会冻结 UI 的、CPU 密集型的计算(对大数据集进行排序或过滤、解析大文件、图像处理、哈希运算等)。
如果你要驱动一个属于自己的、长期存活并带有自身消息机制的 worker 脚本,请参阅 useWebWorker。
在 SSR 或 SSG 模式的服务端,是没有 worker 的:runWorkerFn() 会内联(在同一进程中)运行你的函数并以其结果 resolve,因此在 SSR 期间计算出的值与客户端将会计算出的值是相同的。列在 dependencies 中的脚本在服务端不会被加载。
该 composable 也可以在 setup() 之外调用:在 boot 文件、store 或普通模块中。在那种场景下不会有任何东西自动终止 worker:完成后请调用 terminateWorkerFn()。
语法
import { useWebWorkerFn } from 'quasar'
setup () {
const { workerFnStatus, runWorkerFn, terminateWorkerFn } = useWebWorkerFn(
(a, b) => a + b, // the function to run in the worker
{
// all optional:
timeout: 10000, // ms before a running call gets rejected
dependencies: [ /* ... */ ], // script URLs the function needs
localDependencies: [ /* ... */ ], // your own functions it calls
transfer: (a, b) => [ /* ... */ ], // Transferables among the arguments
onSuccess (result, args) { // called when a call resolves
// ...
},
onError (error, args) { // called when a call rejects with an error
// ...
},
onTimeout (args) { // called when a call hits the timeout
// ...
},
onTerminate (reason) { // called right after the worker got killed
// ...
}
}
)
// ...
}function useWebWorkerFn<Fn extends (...args: any[]) => any>(
fn: Fn,
options?: {
timeout?: number
dependencies?: (string | URL)[]
localDependencies?: Function[]
transfer?: (...args: Parameters<Fn>) => Transferable[]
onSuccess?: (result: Awaited<ReturnType<Fn>>, args: Parameters<Fn>) => void
onError?: (error: unknown, args: Parameters<Fn>) => void
onTimeout?: (args: Parameters<Fn>) => void
onTerminate?: (
reason: 'terminate' | 'timeout' | 'error' | 'unmount'
) => void
}
): {
workerFnStatus: Ref<'idle' | 'running' | 'success' | 'error' | 'timeout'>
runWorkerFn: (...args: Parameters<Fn>) => Promise<Awaited<ReturnType<Fn>>>
terminateWorkerFn: () => void
}runWorkerFn(...args) 会带着这些参数在 worker 中调用你的函数,并以它返回的值 resolve(如果返回的是一个 Promise,则会等待其完成)。它会在以下情况 reject:你的函数抛出了错误(一个 Error 会以 Error 的形式传回,并带上它的 message)、worker 脚本本身加载失败、某个 localDependencies 条目没有名字、调用耗时超过了 timeout、期间调用了 terminateWorkerFn(),以及还有另一个调用正在运行时——同一时刻只能有一个调用,请先 await 它再发起下一个。
workerFnStatus 会跟随最近一次调用:第一次调用之前(以及被终止之后)为 idle,随后依次为 running、success、error 或 timeout。
worker 会在第一次调用时创建,并保留下来供后续调用使用,因此反复调用不会再次付出启动成本。terminateWorkerFn() 会杀掉它(并 reject 正在运行的调用);下一次调用会启动一个全新的 worker。timeout 同样会杀掉它:那个超时的调用其实仍在 worker 内部运行,而且没有别的办法能停下它。当组件被销毁时,该 composable 会自行终止 worker。
这些钩子会报告每一次调用的结果,并连带上发起该调用时所用的参数,这样一个处理函数就能对来自任何地方的调用做出响应:onSuccess(result, args) 在调用 resolve 时触发;onError(error, args) 在调用因错误而 reject 时触发(你的函数抛出了异常、worker 脚本加载失败、无法从一个未命名的 localDependencies 条目构建脚本、或某个参数无法被克隆);onTimeout(args) 在调用超过 timeout 时触发。onTerminate(reason) 会在 worker 被杀掉之后立即触发——排在它所打断的那次调用的结果钩子之后——并以 reason 说明原因:'terminate' 表示一次 terminateWorkerFn() 调用,'timeout' 表示超时,'error' 表示脚本加载失败,'unmount' 表示组件被销毁。由 terminateWorkerFn() 导致的 reject,或由“在另一个调用运行期间发起调用”导致的 reject,并不属于你的函数的执行结果,因此不会有任何钩子去报告它。
函数会被序列化
你的函数会以源代码的形式(通过 Function.prototype.toString())被送往 worker,并在 worker 自己的全局作用域中被求值。这带来了一些后果:
- 它必须是自包含的:不能使用外层作用域中的变量、不能有导入的模块、不能引用组件状态、不能用
$q。它能用的只有:自身的参数、选项中列出的dependencies和localDependencies,以及 worker 本身所提供的能力(fetch()、self、crypto、indexedDB……) - 它的参数和返回值必须是可结构化克隆的(structured-cloneable):普通数据、数组、类型化数组、
Map、Set、Date、Blob、File、ImageData……但不能是函数、DOM 节点、类实例(它们会以普通对象的形式抵达)或 Vue 的响应式代理(请先用toRaw()解包) - 一个无法被克隆的结果或抛出的值,会改为以克隆错误的 message(一个字符串)来 reject
- 该 worker 是一个经典(classic)worker,因此函数中不能使用
import语法(无论静态还是动态);请通过dependencies来加载它所需要的东西
localDependencies 会把你自己的辅助函数(或类)内联进 worker 脚本;它们必须有名字(一个 function 声明,或一个赋值给 const 的箭头函数),并且你的函数要通过该名字来调用它们。dependencies 列出了在你的函数运行之前要通过 importScripts() 加载的脚本;它们必须是经典脚本(不能是 ES 模块),它们在全局作用域上定义的东西随后即可使用。
import { useWebWorkerFn } from 'quasar'
function distance (a, b) {
return Math.hypot(a.x - b.x, a.y - b.y)
}
setup () {
const { runWorkerFn } = useWebWorkerFn(
(points, origin) => points.filter(p => distance(p, origin) < 10),
{ localDependencies: [ distance ] }
)
// ...
}转移(transfer)而非拷贝
参数会被拷贝到 worker 中。对于一个很大的 ArrayBuffer(一张图片的像素、一个文件的内容),你可以改为“移动”它,无论它多大,这几乎不花任何成本:transfer 选项会接收本次调用的参数,并返回要转移的那些对象。一旦被移动,该对象在主线程上就不可用了(它的 byteLength 会变成 0)。
const { runWorkerFn } = useWebWorkerFn(
(pixels, width, height) => {
const view = new Uint8ClampedArray(pixels)
// ...heavy work on view...
return result
},
{ transfer: pixels => [pixels] }
)
await runWorkerFn(imageData.data.buffer, imageData.width, imageData.height)返回值则始终会被拷贝回来。
示例
基础示例:在 worker 中对 200 万个数字进行排序,与在主线程上排序做对比——旋转的齿轮 spinner 在 worker 排序时会持续转动,而在主线程排序时会卡住。
<template>
<div class="q-pa-md">
<div class="row items-center q-gutter-sm q-mb-md">
<q-btn
color="primary"
label="Sort 2 million numbers in a worker"
no-caps
:loading="workerFnStatus === 'running'"
@click="sortInWorker"
/>
<q-btn
color="grey-8"
label="Sort on the main thread"
no-caps
outline
@click="sortInline"
/>
</div>
<div class="row items-center q-gutter-sm q-mb-md">
<q-badge :label="workerFnStatus" color="secondary" />
<div v-if="result !== null">
{{ result.where }}: median {{ result.median }} in {{ result.ms }}ms
</div>
</div>
<q-spinner-gears size="40px" color="primary" />
<div class="text-caption">
The spinner keeps turning while the worker sorts, and freezes when the
main thread does it.
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useWebWorkerFn } from 'quasar'
function medianOfRandom(size) {
const numbers = new Float64Array(size)
for (let i = 0; i < size; i++) {
numbers[i] = Math.random()
}
numbers.sort()
return numbers[size >> 1].toFixed(4)
}
const { workerFnStatus, runWorkerFn } = useWebWorkerFn(medianOfRandom)
const size = 2_000_000
const result = ref(null)
async function sortInWorker() {
const start = performance.now()
const median = await runWorkerFn(size)
result.value = {
where: 'Worker',
median,
ms: Math.round(performance.now() - start)
}
}
function sortInline() {
const start = performance.now()
const median = medianOfRandom(size)
result.value = {
where: 'Main thread',
median,
ms: Math.round(performance.now() - start)
}
}
</script>超时与终止示例:一次调用可以按时完成、可以触发超时,也可以被 terminateWorkerFn() 主动终止。
<template>
<div class="q-pa-md">
<div class="row items-center q-gutter-sm q-mb-md">
<q-btn
color="primary"
label="Run for 1s (timeout: 3s)"
no-caps
:disable="workerFnStatus === 'running'"
@click="run(1000)"
/>
<q-btn
color="orange"
label="Run for 10s (times out)"
no-caps
:disable="workerFnStatus === 'running'"
@click="run(10000)"
/>
<q-btn
v-if="workerFnStatus !== 'idle' && workerFnStatus !== 'timeout'"
color="negative"
label="terminateWorkerFn()"
no-caps
@click="terminateWorkerFn"
/>
</div>
<div class="row items-center q-gutter-sm">
<q-badge :label="workerFnStatus" color="secondary" />
<div>{{ message }}</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useWebWorkerFn } from 'quasar'
const { workerFnStatus, runWorkerFn, terminateWorkerFn } = useWebWorkerFn(
async ms => {
await new Promise(resolve => {
setTimeout(resolve, ms)
})
return `${ms}ms of work done`
},
{ timeout: 3000 }
)
const message = ref('')
async function run(ms) {
message.value = 'running...'
try {
message.value = await runWorkerFn(ms)
} catch (err) {
message.value = err.message
}
}
</script>worker 脚本是从一个 Blob URL 创建的,因此你的 CSP 中的 worker-src 指令(它会回退到 script-src)必须允许 blob:。否则,runWorkerFn() 会以 SecurityError reject。这一点与那些附带严格 CSP 的应用有关,例如带有 Content-Security-Policy meta 标签的 Electron 应用。