有趣的 hook - useWorker

简介: 前几天看到个有趣的 hook: useWorker。可以直接将函数转换为 worker,然后调用执行,这样便可以将一些耗时、阻塞的计算放到 worker 中执行,避免主线程阻塞。由于很好奇这个 hook 如果在不支持 worker 的浏览器上有没有做兼容,就把源码看了一下,这里记录一下。📝

网络异常,图片无法展示
|

前几天看到个有趣的 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 实例
  • isRunningworker 执行状态
  • promise: 保存 worker 执行的 promiseresolvereject,方便调用
  • timeoutId:记录 timeout 定时器的 id,设置 timeout 时使用

还包含了几个方法:

  • setWorkerStatus:用于设置 worker 状态和 isRunning
  • killWorker:用于终止和清理 worker
  • onWorkerEnd: 在 worker 执行完成时调用,会按照 option 判定是否需要清理 worker,并更新状态
  • generateWorker:创建 worker 实例,并与其建立通信。
  • callWorker:调用 worker 执行
  • workerHookuseWorker 返回值之一,用于调用 callWorker

还有一个 effect,就是组件卸载时调用 killWorker 清理 worker

而另一个返回值 workerController 则是包含 statuskillWorker

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,会返回 workerHookworkerController,例子中 workerHook 命名为 sortWorkerworkerController 没用到。

然后在点击按钮时,会调用 runSortrunSort 会调用 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 实例,并绑定 onmessageonerror,并在随后开启超时定时器。

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 返回结果 和调用 onWorkerEndonWorkerEnd 会按照 autoTerminate 参数决定是否需要在完成任务后自动销毁 worker

其中还有一些报错处理、超时处理的代码,就不细说了。

兼容处理

然而没发现兼容相关的代码。useWorker 使用到了 createObjectURLWorker,当然这俩兼容性还可以,兼容的 IE 10。如果不放心可以主动做个降级:

const runSort = async () => {
    try {
        const result = await sortWorker(numbers);
        console.log(result);
    } catch (e) {
        sortNumbers(numbers);
    }
};
复制代码

虽然 hook 外无法包裹条件判断,但由于调用 sortWorker 才会去执行 createObjectURLWorker 实例化,我们在调用时做个判断即可,或者通过前置判断:

const runSort = async () => {
    const result = typeof Worker === 'undefined' ? sortNumbers(numbers) : await sortWorker(numbers);
};
复制代码

总结

useWorker 可以在进行耗能计算时通过 worker 来避免主线程的阻塞,如果在业务中有使用如前端大批量数据搜索、复杂计算时可以考虑使用,可以有效提高代码性能。

其它相似库

如果要在非 react 环境下转换 worker,也可以尝试以下库,或者照着思路自己实现:

相关文章
|
JavaScript
js 使用fetch来上传文件 formdata()
js 使用fetch来上传文件 formdata()
|
5月前
|
Web App开发 人工智能 Android开发
5.3K star!硅基生命新纪元,这个开源数字人框架要火!
"只需3分钟视频素材,就能打造专属数字分身!" "开源免费商用,支持安卓/iOS/Web全平台运行" "法律咨询、虚拟陪伴、教育导师...解锁AI数字人无限可能"
303 5
|
前端开发 JavaScript 大数据
React与Web Workers:开启前端多线程时代的钥匙——深入探索计算密集型任务的优化策略与最佳实践
【8月更文挑战第31天】随着Web应用复杂性的提升,单线程JavaScript已难以胜任高计算量任务。Web Workers通过多线程编程解决了这一问题,使耗时任务独立运行而不阻塞主线程。结合React的组件化与虚拟DOM优势,可将大数据处理等任务交由Web Workers完成,确保UI流畅。最佳实践包括定义清晰接口、加强错误处理及合理评估任务特性。这一结合不仅提升了用户体验,更为前端开发带来多线程时代的全新可能。
364 1
|
存储 前端开发 JavaScript
【亮剑】在React中,处理`onScroll`事件可实现复杂功能如无限滚动和视差效果
【4月更文挑战第30天】在React中,处理`onScroll`事件可实现复杂功能如无限滚动和视差效果。类组件和函数组件都能使用`onScroll`,通过`componentDidMount`和`componentWillUnmount`或`useEffect`添加和移除事件监听器。性能优化需注意节流、防抖、虚拟滚动、避免同步计算和及时移除监听器。实战案例展示了如何用Intersection Observer和`onScroll`实现无限滚动列表,当最后一项进入视口时加载更多内容。合理利用滚动事件能提升用户体验,同时要注意性能优化。
738 0
|
Web App开发 监控 前端开发
React 性能监测工具大揭秘!Chrome DevTools 高级用法来袭,让你的 React 应用性能飙升!
【8月更文挑战第31天】在前端开发中,React 框架虽简化了高效、交互性强的用户界面构建,但应用复杂性增加亦可能引发性能问题。此时,Chrome DevTools 凭其性能面板成为了优化应用性能的重要工具,能帮助开发者记录与分析加载时间、渲染及脚本执行等性能指标,定位并解决性能瓶颈。同时,其 React 开发者扩展工具允许实时监控组件状态变化,进一步提升性能。结合运用这些功能,将有助于打造流畅的用户体验。
347 0
|
监控 安全 Shell
清除阿里云服务器挖矿病毒总结
监控发现`top`命令显示`xmrig`进程占满CPU,确认服务器遭挖矿病毒感染。通过`thistory`追溯到病毒执行步骤,包括下载恶意脚本、设置定时任务等。处理方案包括:清理异常定时任务并修复权限;查找并删除挖矿相关文件;识别并终止可疑`sh`进程;加强SSH安全,如修改端口、清除密钥、限制IP访问等,以彻底清除病毒并加固系统安全。
1546 2
|
前端开发 UED
前端 CSS 经典:在 Vue3 中使用渐进式图片
前端 CSS 经典:在 Vue3 中使用渐进式图片
329 0
|
JavaScript 前端开发 API
快速实现 iframe 嵌套页面
【6月更文挑战第22天】快速实现 iframe 嵌套页面
|
开发工具 git
git将一个远程分支的部分修改提交到另一个远程分支
git将一个远程分支的部分修改提交到另一个远程分支
960 1
|
负载均衡 前端开发 网络协议
Keepalived+HAProxy 搭建高可用负载均衡(二)
Keepalived+HAProxy 搭建高可用负载均衡
761 0

热门文章

最新文章