如何在 React 中使用 web-worker
什么是 web-worker
Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。
在 web-worker 中可以使用什么?
- XMLHttpRequest
- fetch
- localStorage
- indexDB
- 执行其他 JavaScript 代码 ...
不能使用什么?
-
不能操作 DOM(主线程才可以操作)
-
不能使用 window 全局对象
worker 运行在另一个全局上下文中,不同于当前的
window。因此,在 worker 中不能使用 window 对象。
一个 worker 可以将消息发送到创建它的 JavaScript 代码,通过将消息发布到该代码指定的事件处理器(反之亦然)。
workers 和主线程间的数据传递通过这样的消息机制进行——双方都使用 postMessage() 方法发送各自的消息,使用 onmessage 事件处理函数来响应消息(消息被包含在 message 事件的 data 属性中)。这个过程中数据并不是被共享而是被复制。
在 React 中使用 web-worker
原生的 web-worker 虽然好用,但是如果想要在项目中使用,可能会存在以下问题:
- 需要手动修改 webpack 配置
- 需要对消息的发送、处理等大量封装,容易产生非常多的非业务性代码
- 增加同事 Review 代码的难度(是的,你很强,但你也很费队友)
有现成的 React-hooks 库已经帮助我们处理好开发中能遇到的大多数问题,这里推荐使用useWorker。
但在使用之前,先说一下使用它的限制(同时也是 web-worker 的限制):
-
不要在里面访问 dom
js1document.querySelectoAll('#demoId') // not allowed!! 2window.navigator // not allowed!! -
当工作线程运行时,你要等它完成才能继续调用,或者先 kill 它。
-
web worker 无法返回函数,因为返回内容被 messageEvent 传递时序列化成字符串了。
-
web worker 接受到 handler 必须是纯函数而不能有任何副作用(比如在里面 setState 是不行的)。
以下为用法:
安装
bash1npm install --save @koale/useworker
下面是针对 5000000 个数字进行排序的 webworker 使用方法,使用这个方法并不会阻塞 UI。
tsx1import React from 'react' 2import { useWorker } from '@koale/useworker' 3 4const numbers = [...Array(5000000)].map((e) => ~~(Math.random() * 1000000)) 5const sortNumbers = (nums) => nums.sort() 6 7const Example = () => { 8 const [sortWorker] = useWorker(sortNumbers) 9 10 const runSort = async () => { 11 const result = await sortWorker(numbers) // non-blocking UI 12 console.log('End.') 13 } 14 15 return ( 16 <button type="button" onClick={runSort}> 17 Run Sort 18 </button> 19 ) 20}
useWorker 入参和出参
以下是 useWorker 的 TS 定义:
ts1const useWorker: <T extends (...fnArgs: any[]) => any>( 2 fn: T, 3 options?: Options, 4) => [(...fnArgs: Parameters<T>) => Promise<ReturnType<T>>, WorkerController]
我们分析一下:
-
它接受一个函数,这里使用的是泛型,并且限制该泛型为函数,该函数的参数和返回值都为 any
-
它接受一个可选的 options,options 为:
typescript1type Options = { 2 timeout?: number | undefined // kill worker 前的延迟 3 remoteDependencies?: string[] | undefined // 一个数组,包含运行工作程序所需的远程依赖项 4 autoTerminate?: boolean | undefined // 在worker 完成后自动终止 5 transferable?: TRANSFERABLE_TYPE | undefined // 启用可传输对象,若要禁用,请设置可传输:“none” 6}其中比较有疑问的是
remoteDependencies,当传入的 handler 中有一个方法来自于 cdn(或者其他远程链接)时,需要在这里写依赖的远程地址:jsx1import { useWorker } from "@koale/useworker"; 2 3const fn = dates => dates.sort(dateFns.compareAsc) 4 5const [workerFn, {status: workerStatus, kill: workerTerminate }] = useWorker(fn, { 6 timeout: 50000 // 5 seconds 7 remoteDependencies: [ 8 "https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.30.1/date_fns.js" // dateFns 9 ], 10}); -
它返回的第一个出参是 handler 的返回值(
ReturnType<T>),前面之所以用泛型,是因为这里需要用到返回类型。 -
它返回的第二个出参是 WorkerController,例子中没有,这里介绍一下它有哪些东西:
-
status,表示处理的状态,一共有五个:
PENDING:web 工作程序已初始化,但尚未执行SUCCESS:成功RUNNING:运行中ERROR:出错啦TIMEOUT_EXPIRED:因超时而挂掉了 -
kill,用来停止 web-worker 的方法。
-
使用场景
使用场景其实很宽泛,在 React 中只要能使用纯函数,并且做一些操作很重的任务,但是不希望影响 UI,就可以使用这种方式来优化,比较常见的有:
-
CSV 解析和下载(纯前端实现时)
-
某些用来解析的脚本,不希望它们占用主线程运行时。
举个transform:css-to-js的例子,打开它的源码 1、源码 2,你会发现它在解析 css 时,用到的是 worker 内调用 postcss 来将字符串转化为 JS 对象。