前几天看到个有趣的 hook: useWorker。可以直接将函数转换为 worker,然后调用执行,这样便可以将一些耗时、阻塞的计算放到 worker 中执行,避免主线程阻塞。
由于很好奇这个 hook 如果在不支持 worker 的浏览器上有没有做兼容,就把源码看了一下,这里记录一下。📝
源码解析
由于库很小,文件就不看了,直接看下导出:
导出
export { useWorker } from './useWorker'; export { WORKER_STATUS } from './lib/status'; 复制代码
导出一共就两个,一个 useWorker hook 主体,一个是 WORKER_STATUS 常量,里面包含几种状态:
export enum WORKER_STATUS { PENDING = 'PENDING', SUCCESS = 'SUCCESS', RUNNING = 'RUNNING', ERROR = 'ERROR', TIMEOUT_EXPIRED = 'TIMEOUT_EXPIRED' } 复制代码
useWorker 定义和实现
先看下 useWorker 定义:
type Options = { timeout?: number; remoteDependencies?: string[]; autoTerminate?: boolean; transferable?: TRANSFERABLE_TYPE; }; export const useWorker = <T extends (...fnArgs: any[]) => any>(fn: T, options: Options = DEFAULT_OPTIONS) => [ typeof workerHook, WorkerController ]; 复制代码
在看下实现,useWorker 包含一个 state workerStatus,默认为 PENDING。
包含四个 ref:
worker:创建的worker实例isRunning:worker执行状态promise: 保存worker执行的promise的resolve和reject,方便调用timeoutId:记录timeout定时器的id,设置timeout时使用
还包含了几个方法:
setWorkerStatus:用于设置worker状态和isRunningkillWorker:用于终止和清理workeronWorkerEnd: 在worker执行完成时调用,会按照option判定是否需要清理worker,并更新状态generateWorker:创建worker实例,并与其建立通信。callWorker:调用worker执行workerHook:useWorker返回值之一,用于调用callWorker
还有一个 effect,就是组件卸载时调用 killWorker 清理 worker。
而另一个返回值 workerController 则是包含 status 和 killWorker
const workerController = { status: workerStatus, kill: killWorker }; 复制代码
执行流程
我们先看下使用方法,然后配合看下代码如何运行:
import React from 'react'; import { useWorker } from '@koale/useworker'; const numbers = [...Array(5000000)].map(e => ~~(Math.random() * 1000000)); const sortNumbers = nums => nums.sort(); const Example = () => { const [sortWorker] = useWorker(sortNumbers); const runSort = async () => { const result = await sortWorker(numbers); console.log(result); }; return ( <button type='button' onClick={runSort}> Run Sort </button> ); }; 复制代码
使用时首先调用 useWorker,会返回 workerHook 和 workerController,例子中 workerHook 命名为 sortWorker,workerController 没用到。
然后在点击按钮时,会调用 runSort,runSort 会调用 workerHook 并传入 numbers。看下 workerHook 的源码。
const workerHook = React.useCallback( (...fnArgs: Parameters<T>) => { const terminate = options.autoTerminate != null ? options.autoTerminate : DEFAULT_OPTIONS.autoTerminate; if (isRunning.current) { /* eslint-disable-next-line no-console */ console.error( '[useWorker] You can only run one instance of the worker at a time, if you want to run more than one in parallel, create another instance with the hook useWorker(). Read more: https://github.com/alewin/useWorker' ); return Promise.reject(); } if (terminate || !worker.current) { worker.current = generateWorker(); } return callWorker(...fnArgs); }, [options.autoTerminate, generateWorker, callWorker] ); 复制代码
他会先判定 terminate 参数,用于判定是否需要自动回收。然后判定 isRunning,避免重复执行。然后判定是否存在 worker 实例,不存在则调用 generateWorker 创建。随后便将传入的参数传递给 callWorker。
再看下 generateWorker 的源码。
const generateWorker = useDeepCallback(() => { const { remoteDependencies = DEFAULT_OPTIONS.remoteDependencies, timeout = DEFAULT_OPTIONS.timeout, transferable = DEFAULT_OPTIONS.transferable } = options; const blobUrl = createWorkerBlobUrl(fn, remoteDependencies!, transferable!); const newWorker: Worker & { _url?: string } = new Worker(blobUrl); newWorker._url = blobUrl; newWorker.onmessage = (e: MessageEvent) => { const [status, result] = e.data as [WORKER_STATUS, ReturnType<T>]; switch (status) { case WORKER_STATUS.SUCCESS: promise.current[PROMISE_RESOLVE]?.(result); onWorkerEnd(WORKER_STATUS.SUCCESS); break; default: promise.current[PROMISE_REJECT]?.(result); onWorkerEnd(WORKER_STATUS.ERROR); break; } }; newWorker.onerror = (e: ErrorEvent) => { promise.current[PROMISE_REJECT]?.(e); onWorkerEnd(WORKER_STATUS.ERROR); }; if (timeout) { timeoutId.current = window.setTimeout(() => { killWorker(); setWorkerStatus(WORKER_STATUS.TIMEOUT_EXPIRED); }, timeout); } return newWorker; }, [fn, options, killWorker]); 复制代码
此处使用的是自定义 hookuseDeepCallback,他会深比对 dependences 来触发 callback 的更新。
可以看到主要是调用了 createWorkerBlobUrl 创建了一个 worker url,然后创建 worker 实例,并绑定 onmessage 和 onerror,并在随后开启超时定时器。
createWorkerBlobUrl 代码就三句:
const blobCode = ` ${remoteDepsParser(deps)}; onmessage=(${jobRunner})({ fn: (${fn}), transferable: '${transferable}' }) `; const blob = new Blob([blobCode], { type: 'text/javascript' }); const url = URL.createObjectURL(blob); 复制代码
显示将 jobRunner、fn、transferable 拼接成一段方法字符串,然后创建 blob 并将其转换为 url。
jobRunner 会调用 fn,然后将 fn 返回的结果和状态通过 postMessage 发送给主线程,主线程会触发 onmessage,调用 promiseRef 返回结果 和调用 onWorkerEnd。onWorkerEnd 会按照 autoTerminate 参数决定是否需要在完成任务后自动销毁 worker。
其中还有一些报错处理、超时处理的代码,就不细说了。
兼容处理
然而没发现兼容相关的代码。useWorker 使用到了 createObjectURL 和 Worker,当然这俩兼容性还可以,兼容的 IE 10。如果不放心可以主动做个降级:
const runSort = async () => { try { const result = await sortWorker(numbers); console.log(result); } catch (e) { sortNumbers(numbers); } }; 复制代码
虽然 hook 外无法包裹条件判断,但由于调用 sortWorker 才会去执行 createObjectURL 和 Worker 实例化,我们在调用时做个判断即可,或者通过前置判断:
const runSort = async () => { const result = typeof Worker === 'undefined' ? sortNumbers(numbers) : await sortWorker(numbers); }; 复制代码
总结
useWorker 可以在进行耗能计算时通过 worker 来避免主线程的阻塞,如果在业务中有使用如前端大批量数据搜索、复杂计算时可以考虑使用,可以有效提高代码性能。
其它相似库
如果要在非 react 环境下转换 worker,也可以尝试以下库,或者照着思路自己实现: