Web Worker使用新姿势

前言

大家都知道,为了防止多个线程同时操作DOM,带来渲染冲突问题,所以js执行器被设计成单线程,但是随着时间的推移,js也会逐渐涉及到大计算的场景(比如大数据计算,图片、视频、音频处理等等),在这样的场景下,单线程就会被长时间阻塞,甚至造成页面卡顿,影响用户体验。

Web Worker就是为了解决单线程这一弊端形成的产物,它允许我们在js主线程之外开辟新的Worker线程,并将一段js脚本运行其中,它赋予了开发者利用js操作多线程的能力。因为是独立的线程,Worker线程与js主线程能够同时运行,互不阻塞。所以,在我们有大量运算任务时,可以把运算任务交给Worker线程去处理,当Worker线程计算完成,再把结果返回给js主线程即可。

worker基础知识

这里举个例子,我们要对一个长度为5000000的数组进行排序,如果直接在js中执行,会影响页面的UI渲染,伴有明显卡顿现象,接下来我们用worker来实现这个功能,大概了解一下worker如何创建,以及worker和主进程之间如何通信。

  1. 先创建一个worker.js文件,这个worker接收一个数组,将这个数组排序后进行输出:
js 复制代码
// worker.js
onmessage = async (e) => {
  const nums = e.data;
  postMessage(nums.sort());
};
  1. 在主文件中创建worker,再将业务数据(numbers)传到worker进行执行:
js 复制代码
// main.js
const numbers = [...Array(5000000)].map(() => (Math.random() * 1000000));
const onWorkerSort = () => {
  const worker = new Worker('worker.js'); // 创建worker
  
  worker.postmessage(numbers); // 传递业务数据到worker,worker开始执行
  
  worker.onmessage = (e) => { // 监听worker的执行结果
    console.log('排序结果', e.data);
  };
};
  1. 如果我们想要关闭worker,可以使用如下方法:
js 复制代码
// main.js
myWorker.terminate(); // 关闭worker
js 复制代码
// worker.js
close(); // 直接执行close方法就ok了

worker实际运用

js 复制代码
const worker = new Worker(path, options);

path是有效的js脚本的地址,并且值得一提的是,地址必须遵守同源策略

所以如果我们这样使用,本地测试可以,但是发布之后会有问题。假设运行环境的域名是a.com,但是test.worker.js是部署在cdn的,所以会存在跨域的问题。

js 复制代码
// webpack配置多入口
module.exports = () => {
  extry: {
    './src/index.js': 'app.js',
    './worker/test.worker.js': 'test.worker.js'
  }
};

// 主js文件使用
const worker = new Worker('./test.worker.js');

开源的useWorker

github.com/alewin/useW...

js 复制代码
import React from "react";
import { useWorker } from "@koale/useworker";

const numbers = [...Array(5000000)].map(() => (Math.random() * 1000000));
const sortNumbers = nums => nums.sort();

const Example = () => {
  const [sortWorker] = useWorker(sortNumbers);

  const runSort = async () => {
    const result = await sortWorker(numbers); // non-blocking UI
    console.log(result);
  };

  return (
    <button type="button" onClick={runSort}>
      Run Sort
    </button>
  );
};

这是官方的一个例子,使用会报如下错误,也没有去深究为啥报错,感觉像是传入一个函数,重新执行的时候上下文环境不一样了。

使用新姿势(自建useWorker)

Blob URL

从上图可以看出,Worker构造函数接收的path可以是一个Blob URL,那我们就来看看Blob URL是什么东西,并且如何创建。

从上面两个图可以得出,可以通过URL.createObjectURL()方法来创建一个Blob URL,并且这个值可以用于Web Worker,正好完美适配。

useWorker

js 复制代码
// content 就是worker文件的内容字符串
const url = URL.createObjectURL(new Blob([content], { type: 'text/javascript' }));
const worker = new Worker(url);

这样就可以将worker文件的内容构建到源码当中,不用单独发布一个js文件了。为了进一步弱化worker相互传递事件的复杂度,在业务开发中使用简单,还可以进一步封装,我们来自己实现一个useWorker,核心代码如下:

js 复制代码
const createWorkerCode: (code) => {
  code = code.replace('export default ', 'const __worker_run = '); // worker文件内容需要约定一种格式
  return (`
    onmessage = async (e) => {
      ${code}
      const __worker_result = await __worker_run(e.data);
      postMessage(__worker_result);
    };
  `);
};


const useWorker = (code, config = {}) => {
  const {
    params,
    closeWorkerOnUnmount = true, // hooks卸载时是否结束worker
    manual = false, // 是否手动执行worker
  } = config;
  const [data, setData] = React.useState();

  React.useEffect(() => {
    const content = createWorkerCode(code);
    const url = URL.createObjectURL(new Blob([content], { type: 'text/javascript' }));
    const worker = new Worker(url);
    worker.onmessage = (e) => { // 监听worker接收事件,然后更新result
      setData(e.data);
    };
    return () => {
      closeWorkerOnUnmount && worker.terminate(); // hooks卸载时是否结束worker
    };
  }, [code]);

  React.useEffect(() => {
    !manual && worker.postMessage(params); // 执行worker文件并给worker传参
  }, [JSON.stringify(params)]);

  // 手动执行worker逻辑
  const run = (p) => {
    worker.postMessage(p);
  };

  return {
    data, // worker执行的结果
    run, // 手动执行worker
    worker, // worker实例
  };
};

使用

上面大数据排序的实例可以使用自建useWorker轻松实现:

js 复制代码
// worker.js
export default (nums) => {
  reutrn nums.sort();
}
js 复制代码
// main.js
import workerCode from './worker.js'; // workerCode是文件内容字符串

const numbers = [...Array(5000000)].map(() => (Math.random() * 1000000));
const App = () => {
  const { data, run } = useWorker(workerCode, { manual: true })
  console.log('排序结果:' data);

  return (
    <Button onClick={() => run(numbers)}>大数据排序</Button>
  )
}

这里还要提到一点,我们针对worker.js的文件使用了raw-loader进行处理,所以此类文件在import的时候不会使用babel进行编译,而是直接返回内容字符串,配置如下:

js 复制代码
webworker: {
  test: /\.worker\.(js|ts)$/i,
  use: [{
    loader: 'raw-loader',
  }],
},

总结

  1. worker的使用成本主要有几点,第一是通信复杂,需要频繁在主进程和worker之间调用postmessage和onmessage,容易绕晕;第二是worker如果为单独的js文件,需要解决跨域问题。
  2. 本文通过自建useWorker来解决上面两个问题,将通信封装在内部,且将worker代码也打包到项目bundle中。自然在使用上面也存在一定限制,比如worker文件里面必须是 export default () => {} 的格式,另外是项目在处理worker.js文件时需要借助raw-loader
  3. 最后worker不是万能的,一般场景是用于大数据的计算和处理,它还是存在一些限制,比如worker文件只能使用原生的js语法,不能操作dom等等。
相关推荐
Cutey91622 分钟前
前端如何实现菜单的权限控制(RBAC)
前端·javascript·设计模式
理查der驾23 分钟前
mini-react 第七天:实现useEffect
react.js
程序视点2 小时前
Java中JDK里用到了哪些设计模式?让面试官眼前一亮!
java·设计模式
码农不惑2 小时前
Qt开发:QtWebEngine中操作选择文本
开发语言·javascript·qt·web
疏狂难除3 小时前
基于SeaORM+MySQL+Tauri2+Vite+React等的CRUD交互项目
前端·react.js·前端框架
wkj0013 小时前
js给后端发送请求的方式有哪些
开发语言·前端·javascript
magic 2453 小时前
JavaScript运算符与流程控制详解
开发语言·前端·javascript
xulihang3 小时前
在手机浏览器上扫描文档并打印
前端·javascript·图像识别
帅比九日4 小时前
【canvas】一键自动布局:如何让流程图节点自动找到最佳位置
前端·javascript·算法
超能996要躺平4 小时前
js判断浏览器窗口回话关闭与刷新
前端·javascript