多线程
Worker 接口是 Web Workers API 的一部分,指的是一种可由脚本创建的后台任务,任务执行中可以向其创建者收发信息。要创建一个 Worker,只须调用 Worker(URL) 构造函数,函数参数 URL 为指定的脚本。
封装
interface WebWorkerFn<T = any, CTX extends any[] = any[]> {
/*promiseFn 使用到的上下文函数*/
localDeps?: Fn[]
/*通过 http 链接引入的全局变量*/
deps?: string[]
promiseFn?: (...ctx: CTX) => Promise<T>
/* 作为 promiseFn 的入参*/
ctx?: CTX
}
type WebWorkerFnInit<T = any, CTX extends any[] = any[]> = PartRequired<
WebWorkerFn<T, CTX>,
'promiseFn'
>
/*
* @example
* webWorkerFn(
async () => {
await sleep(1e3)
console.log('sleep done', dateFns)
},
{
localDeps: [sleep],
deps: [
'https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.30.1/date_fns.js', // dateFns
],
}
).then(async (r) => {
console.log(r)
})
* */
export async function webWorkerFn<T = any, CTX extends any[] = any[]>(
promiseFn: WebWorkerFnInit<T, CTX>['promiseFn'],
init?: WebWorkerFn<T, CTX>
): Promise<T>
export async function webWorkerFn<T = any, CTX extends any[] = any[]>(
init: WebWorkerFnInit<T, CTX>
): Promise<T>
export async function webWorkerFn<T = any, CTX extends any[] = any[]>(
promiseFn: WebWorkerFnInit<T, CTX>['promiseFn'] | WebWorkerFnInit<T, CTX>,
init?: WebWorkerFn<T, CTX>
): Promise<T> {
const fullInit = (
typeof promiseFn === 'function'
? {
promiseFn,
...(init || {}),
}
: promiseFn
) satisfies WebWorkerFnInit<T, CTX>
const depsString = (fullInit.deps || []).map((dep) => `'${dep}'`).toString()
const depsFunctionString = (fullInit.localDeps || [])
.filter((dep) => typeof dep === 'function')
.map((fn) => {
const str = fn.toString()
if (str.trim().startsWith('function')) {
return str
} else {
const name = fn.name
return `const ${name} = ${str}`
}
})
.join(';')
const { promise, resolve, reject } = Promise.withResolvers<T>()
const workerBlob = new Blob(
[
`
${depsString.trim() === '' ? '' : `importScripts(${depsString});`}
${depsFunctionString}
const fn = ${String(promiseFn)}
onmessage = function (event) {
if(!event.data) return
const { action, payload } = event.data
if(action === 'initial'){
fn(...payload)
.then((payload)=>{
postMessage({
action: 'resolve',
payload,
});
})
.catch((e)=>{
postMessage({
action: 'error',
payload: e,
})
})
}
};
`,
],
{ type: 'text/javascript' }
)
const workerUrl = URL.createObjectURL(workerBlob)
const worker = new Worker(workerUrl)
worker.onmessage = (event) => {
const { action, payload } = event.data as IResponse
if (action === 'error') {
return reject(payload)
}
if (action === 'resolve') {
return resolve(payload)
}
}
worker.postMessage({
action: 'initial',
payload: fullInit.ctx,
})
return promise.finally(() => {
URL.revokeObjectURL(workerUrl)
})
}
interface IResponse<T = any> {
action: 'initial' | 'error' | 'resolve'
payload: T
}// 官方实例
const myWorker = new Worker('worker.js')
const first = document.querySelector('#number1')
const second = document.querySelector('#number2')
first.onchange = function () {
myWorker.postMessage([first.value, second.value])
console.log('Message posted to worker')
}// vueuse 中的实现
// const { workerFn } = useWebWorkerFn(() => {
// some heavy works to do in web worker
// })
// 返回的 workerFn,每次执行都生成新的 webwork 和 promise,那么就可以每次都可以 await workerFn() 来拿到执行结果
function createWorkerBlobUrl(fn: Function, deps: string[]) {
const blobCode = `${depsParser(deps)}; onmessage=(${jobRunner})(${fn})`
const blob = new Blob([blobCode], { type: 'text/javascript' })
const url = URL.createObjectURL(blob)
return url
}
const blobUrl = createWorkerBlobUrl(fn, dependencies)
const newWorker: Worker & { _url?: string } = new Worker(blobUrl)
newWorker.onmessage = (ev) => {}// 简单实现
const script = `
// 数组长度过大很容易导致浏览器假死,等一会就行
const array = Array.from({ length: 1000000 }, value => Math.ceil(Math.random() * 1000));
function Something() {
console.time('worker: Map运行耗时');
const newArray = array.map(x => x * 2 );
console.timeEnd('worker: Map运行耗时');
return postMessage(newArray);
}
// worker发送消息
postMessage("worker已就绪!");
// 为onmessage赋予事件处理函数
onmessage = function (event) {
event.data == 'run' && postMessage("worker收到指令,开始运行!"),Something();
};`
const workerBlob = new Blob([script], { type: 'text/javascript' })
const workerUrl = URL.createObjectURL(workerBlob)
const myWorker = new Worker(workerUrl)
// 主线程接收消息
myWorker.onmessage = function (event) {
console.log(`主线程收到信息:${event.data}`)
}
// 主线程发送消息
myWorker.postMessage('run')Transferable Objects(可转移对象)
reference
在 JavaScript 中,Transferable Objects(可转移对象) 是一种用于在多线程环境(如主线程与 Web Worker)间高效传递数据的机制。它通过转移对象的所有权而非复制数据,显著减少了内存开销和通信延迟,尤其适合处理大型二进制数据(如图像、音频缓冲区等)。以下是其核心特性和工作原理:
概念
所有权转移:Transferable Objects 允许将对象(如 ArrayBuffer、ImageBitmap)的所有权从一个执行上下文(如主线程)转移到另一个(如 Worker 线程),原上下文中的对象会被“剥离”(不可再访问),而接收方获得独占访问权。这种方式避免了数据复制,提升了性能。
支持的类型:仅限特定二进制数据类型,包括 ArrayBuffer、MessagePort和 ImageBitmap
使用方式
js// 主线程代码 const buffer = new ArrayBuffer(1024) worker.postMessage(buffer, [buffer]) // 转移所有权(多了第二个参数)转移后,主线程的 buffer将无法访问,而 Worker 线程通过 onmessage接收到的 event.data即为该 buffer的实例
性能优势
低延迟:相比默认的结构化克隆算法(深拷贝),转移所有权仅需约 5ms(测试中复制 1.8MB 数据耗时 145ms,而转移仅 5ms)
原子性:转移过程是原子的,确保数据一致性,避免竞争条件
与共享内存的区别
SharedArrayBuffer:允许多线程共享同一内存区域,需配合 AtomicsAPI 保证线程安全,适合高频读写场景
Transferable Objects:转移后仅接收方可访问,更适合一次性传递大型数据且无需后续共享的场景
注意事项
不可逆性:所有权转移后,原上下文无法恢复对数据的访问
兼容性:需注意浏览器对特定类型的支持(如 MessagePort的兼容性)
