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等等。
相关推荐
悦涵仙子43 分钟前
CSS中的变量应用——:root,Sass变量,JavaScript中使用Sass变量
javascript·css·sass
兔老大的胡萝卜43 分钟前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
cs_dn_Jie4 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic5 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿5 小时前
webWorker基本用法
前端·javascript·vue.js
清灵xmf6 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
WaaTong6 小时前
《重学Java设计模式》之 单例模式
java·单例模式·设计模式
小白学大数据6 小时前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_390161776 小时前
防抖函数--应用场景及示例
前端·javascript
334554327 小时前
element动态表头合并表格
开发语言·javascript·ecmascript