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等等。
相关推荐
HEX9CF14 分钟前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
积水成江44 分钟前
关于Generator,async 和 await的介绍
前端·javascript·vue.js
Z3r4y1 小时前
【Web】portswigger 服务端原型污染 labs 全解
javascript·web安全·nodejs·原型链污染·wp·portswigger
人生の三重奏1 小时前
前端——js补充
开发语言·前端·javascript
Tandy12356_1 小时前
js逆向——webpack实战案例(一)
前端·javascript·安全·webpack
TonyH20021 小时前
webpack 4 的 30 个步骤构建 react 开发环境
前端·css·react.js·webpack·postcss·打包
老华带你飞1 小时前
公寓管理系统|SprinBoot+vue夕阳红公寓管理系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·spring boot·课程设计
qbbmnnnnnn2 小时前
【WebGis开发 - Cesium】如何确保Cesium场景加载完毕
前端·javascript·vue.js·gis·cesium·webgis·三维可视化开发
f8979070703 小时前
layui动态表格出现 横竖间隔线
前端·javascript·layui
二十雨辰3 小时前
[uni-app]小兔鲜-04推荐+分类+详情
前端·javascript·uni-app