Skip to content

多线程

Worker 接口是 Web Workers API 的一部分,指的是一种可由脚本创建的后台任务,任务执行中可以向其创建者收发信息。要创建一个 Worker,只须调用 Worker(URL) 构造函数,函数参数 URL 为指定的脚本。

封装
ts
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
}
javascript
// 官方实例
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')
}
typescript
// 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) => {}
javascript
// 简单实现
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(可转移对象)

在 JavaScript 中,​​Transferable Objects​(可转移对象)​ 是一种用于在多线程环境(如主线程与 Web Worker)间高效传递数据的机制。它通过转移对象的所有权而非复制数据,显著减少了内存开销和通信延迟,尤其适合处理大型二进制数据(如图像、音频缓冲区等)。以下是其核心特性和工作原理:

  1. 概念

    • 所有权转移​​:Transferable Objects 允许将对象(如 ArrayBuffer、ImageBitmap)的所有权从一个执行上下文(如主线程)转移到另一个(如 Worker 线程),原上下文中的对象会被“剥离”(不可再访问),而接收方获得独占访问权。这种方式避免了数据复制,提升了性能。

    • 支持的类型​​:仅限特定二进制数据类型,包括 ArrayBuffer、MessagePort和 ImageBitmap

  2. 使用方式

    js
    // 主线程代码
    const buffer = new ArrayBuffer(1024)
    worker.postMessage(buffer, [buffer]) // 转移所有权(多了第二个参数)

    转移后,主线程的 buffer将无法访问,而 Worker 线程通过 onmessage接收到的 event.data即为该 buffer的实例

  3. 性能优势

    • 低延迟​​:相比默认的结构化克隆算法(深拷贝),转移所有权仅需约 5ms(测试中复制 1.8MB 数据耗时 145ms,而转移仅 5ms)

    • 原子性​​:转移过程是原子的,确保数据一致性,避免竞争条件

  4. ​与共享内存的区别​

    • ​​SharedArrayBuffer​​:允许多线程共享同一内存区域,需配合 AtomicsAPI 保证线程安全,适合高频读写场景

    • Transferable Objects​​​:转移后仅接收方可访问,更适合一次性传递大型数据且无需后续共享的场景

  5. 注意事项​

    • 不可逆性​​​:所有权转移后,原上下文无法恢复对数据的访问

    • 兼容性​​​:需注意浏览器对特定类型的支持(如 MessagePort的兼容性)