动态创建 Web Worker 实践指南

更新日期: 2020-01-17阅读: 2k标签: Worker

作为前端,在消费接口提供的数据时,往往由于数据实际分布在不同地方(如一部分存储在 ODPS ,而另一部分可能更适合在应用初始化时从本地载入内存)而需要对数据进行区分处理。当然,交互的实现可能也会需要很重的计算逻辑,而为了加速计算、不阻塞渲染线程,Web Worker 不失为一个很好的选择。

网上有很多关于 Web Worker 的入门介绍与示例,但要在实际工程中引入,往往还需要一些额外的工作。相比 MDN 上提供的 demo 示例,在实际工程中,我们可能更希望 Web Worker 能解决如下几个问题:

  • 可根据不同业务属性动态创建不同的 Web Worker;
  • api 简单易用,例如通过 Promise 链式调用替换相对较为繁琐的事件监听处理逻辑;
  • 代码可复用,且最好不需对项目所依赖的构建工具进行更改;

本文结构如下:

  1. 一些约定
  2. 谈及 Web Worker
  3. Web Worker 构造方法举例
  4. 动态 Worker 的简单封装
  5. 调用区分优化
  6. 总结
  7. 进一步阅读

以下开始正文。


0 / 一些约定

教程将教你写一个可动态创建的可复用 Web Worker。在进一步阅读之前,我假设你已经掌握了关于 Web Worker 的一些基本用法,否则建议先阅读 MDN 提供的使用 Web Workers 文档,了解一些基础概念。

与此同时,本文所创建的动态 Worker 均指专有 Worker,不涉及到共享 Worker 及其他类型 Worker 的内容。


1 / 谈及 Web Worker

我们常说「任何可以使用 JavaScript 来编写的应用,最终会由 JavaScript 编写」,但实际移植到 JavaScript 环境时仍然存在很多制约,比如浏览器兼容性、静态类型与运行性能等。 随着 JavaScript 引擎不断地优化,性能已不再是那个最大的瓶颈,而受到浏览器 JavaScript 运行时的单线程环境限制,最大的阻碍貌似来自语言本身。

好在从 html5 规范开始,Web Worker 概念地引入为 JavaScript 引入了线程技术。Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法,你可以在后台执行任务而不干扰用户界面。例如触发长时间运行的脚本以处理计算密集型任务,同时却不会阻碍 UI 或其他脚本处理用户互动。对于 Web Worker 来说,最正常的创建方式无异于创建两个 js 文件,一个 main.js 用于 Worker() 构造器并处理与 worker 间消息的接受与发送:

let myWorker = new Worker('worker.js');

myWorker.postMessage('Hi, this is a message from main.js');

myWorker.onmessage = (e) => {
  console.log('Message received from worker', JSON.stringify(e));
}

一个 worker.js 用于响应主线程的消息,包括处理与回传结果:

onmessage = (e) => {
  console.log('Message received from main script');

  let workerResult = `Result: ${JSON.stringify(e)}`;
  console.log('Posting message back to main script');

  postMessage(workerResult);
}


2 / Web Worker 构造方法举例

在上例中,传入 Worker 构造函数的参数是一个具体路径,但除此外,我们还能传入其他路径达到创建 Web Worker 的目的,比如字符串,具体有如下三种方法:

  • Blob: 该方式适用于 Chrome 8+, Firefox 6+, Safari 6.0+, Opera 15+ 等环境
  • data:application/javascript: 该方式适用于 Opera 10.60 - 12
  • eval: 适用于其他环境,比如 IE 10+

什么是 Blob 对象?它表示一个不可变、原始数据的类文件对象,但不局限于 JavaScript 原生格式的数据,常被用来存储体量很大的二进制编码格式的数据。你可以使用 Blob() 构造函数从一段字符串中创建一个 Blob 对象:

const debug = {hello: "world"};
const blob = new Blob(
  [JSON.stringify(debug, null, 2)],
  {type : 'application/json'}
);

而利用 URL.createObjectURL() API 我们可以将 Blob 对象转换为一个对象 URL 传入 Worker 构造函数,如下:

const worker = new Worker(URL.createObjectURL(blob));

那么什么又是 data:application/javascript 呢?Data URLs,即前缀为 data: 协议的的URL,其允许内容创建者向文档中嵌入小文件。这样的 URL 由四个部分组成:前缀(data:)、指示数据类型的 MIME 类型、如果非文本则为可选的 base64 标记、数据本身:

data:[<mediatype>][;base64],<data>

所以现在你应该知道 application/javascript 所指了,是的,利用这个 URL 我们可以这样创建 Web Worker:

const response = "onmessage=function(e){postMessage('Worker: '+e.data);}";

const worker = new Worker(
  'data:application/javascript,' + encodeURIComponent(response) 
);

worker.onmessage = (e) => {
  alert('Response: ' + e.data);
};

worker.postMessage('Test');

而在 Safari (<6) 与 IE 10 中,eval 作为向后兼容的一种方式,你可以这样创建 Web Worker:

const response = "onmessage=function(e){postMessage('Worker: '+e.data);}";

const worker = new Worker('Worker-helper.js');

worker.postMessage(response);

其中 Worker-helper.js 代码如下:

onmessage = (e) => {
    onmessage = null; // Clean-up
    eval(e.data);
};

当然,在使用之前还需要对相应 API 进行兼容判断,比如 window.URL || window.webkitURL 或者 window.Worker 等,这里便不详述。


3 / 动态 Worker 的简单封装

我们先来写一个简单的 Web Worker 示例,假设我们在 Worker 收到数据时有一个简单的判断逻辑,即只处理 method='format' 的消息:

window.URL = window.URL || window.webkitURL;

const response = `onmessage = ({ data: { data } }) => {
  console.log('Message received from main script');
  const {method} = data;
  if (data.data && method === 'format') {
    postMessage({
      data: {
        'res': 'I am a customized result string.',
      }
    });
  }
  console.log('Posting message back to main script');
}`;
const blob = new Blob([response], {type: 'application/javascript'});

const worker = new Worker(
  URL.createObjectURL(blob)
);

// 事件处理
worker.onmessage = (e) => {
  alert(`Response: ${JSON.stringify(e)}`);
};
worker.postMessage({
  method: 'format', 
  data: []
});

这个 Demo 会建立一个 Web Worker 并向其发送一段文本,而 Worker 在处理完毕后主线程会把结果弹窗显示出来。接下来,我们就用它继续操作。

一个动态 Worker 结构应该长成如下这样,包含构造函数、动态调用函数以及 Worker 销毁函数,而构造函数中至少应该定义好 Worker 用到的全局变量、数据处理函数以及 onmessage 事件处理函数:

const BASE_DATASETS = '';
class DynamicWorker {
  constructor(worker) {
    /**
     * 依赖的全局变量声明
     * 如果 BASE_DATASETS 非字符串形式,可调用 JSON.stringify 等方法进行处理
     * 保证变量的正常声明
     */
    const CONSTS = `const base = ${BASE_DATASETS};`;
    
    /**
     * 数据处理函数
     */ 
    const formatFn = `const formatFn = ${worker.toString()};`;
    
    /**
     * 内部 onmessage 处理
     */
    const onMessageHandlerFn = `self.onmessage = ()=>{}`;

    /**
     * 返回结果
     * @param {*} param0 
     */
    const handleResult = () => {}
    
    const blob = new Blob(
      [`(()=>{${CONSTS}${formatFn}${onMessageHandlerFn}})()`], 
      { type: 'text/javascript' }
    );
    this.worker = new Worker(URL.createObjectURL(blob));
    this.worker.addEventListener('message', handleResult);

    URL.revokeObjectURL(blob);
  }

  /**
   * 动态调用
   */
  send(data) {}

  close() {}
}

以上代码有几点需要解释下,比如生成 Blob 对象时,由于入参是字符串数组,如果只是调用 .toString(),便无法拿到函数名,因此所有字符串采用变量命名的形式定义。接着我们调用 URL.createObjectURL 生成对象 URL,在创建完 Worker 后调用 URL.revokeObjectURL() 让浏览器知道不再需要对这个文件保持引用。

URL.revokeObjectURL() 静态方法用来释放一个之前通过调用 URL.createObjectURL() 创建的已经存在的 URL 对象。当你结束使用某个 URL 对象时,应该通过调用这个方法来让浏览器知道不再需要保持这个文件的引用了。详见 MDN API.

内部接收与响应消息的函数应该做逻辑判断并发送对应信息返回主线程,我们这样完善 onMessageHandlerFn:

const onMessageHandlerFn = `self.onmessage = ({ data: { data } }) => {
      console.log('Message received from main script');
      const {method} = data;
      if (data.data && method === 'format') {
        self.postMessage({
          data: formatFn(data.data)
        });
      }
      console.log('Posting message back to main script');
}`;

利用 Promise 的链式调用,我们可以隐藏较为琐碎的事件监听处理程序。来写一个 send 方法允许开发者动态调用,内部我们接收到数据后,改变 resolve 的状态,并返回这个 Promise:

send(data) {
    const w = this.worker;
    w.postMessage({
      data,
    });

    return new Promise((res) => {
      this.resolve = res;
    })
}

我们定义一个 this.resolve 用于记录 Promise 的状态,然后在 Worker 收到响应后便判断 this.resolve 然后决定是否 resolve 计算结果:

const handleResult = ({ data: { data } }) => {
      if (this.resolve) {
        resolve(data);
        this.resolve = null;
      }
}

如此一来,接下来我们就可以在主进程中这样调用 DynamicWorker 了:

import DataWorker from './dynamicWorker.js';

const formatFunc = () => {
  return {
    'res': 'I am a customized result string.',
  }
}

const worker = new DataWorker(formatFunc);

const result = []; // demo 数据

worker.send({
  method: 'format', 
  data: result
}).then((e) => {
  alert(`Response: ${JSON.stringify(e)}`);
})


4 / 调用区分优化

当然,如果我们没有频繁调用 Worker,那么上面的代码貌似已经足够。但如果你需要短时间多次传输数据进行处理,那么调用的多个方法与对应的多个结果间可能会相互混淆。为什么呢,原因在于我们在构造函数中写的这行:

this.worker.addEventListener('message', handleResult);

这个事件监听处理函数是区分不出每次调用的,在收到消息后它只会执行 resolve。那么该如何解决呢?其实也较为简单,加入一个标志位用于区分不同调用即可。

首先,在构造函数里我们加上这么一行:

this.flagMapping = {};

简单起见,我们直接取日期作为标志位 key,改写后的 send 方法长成这样:

send(data) {
    const w = this.worker;
    const flag = new Date().toString();
    w.postMessage({
      data,
      flag,
    });

    return new Promise((res) => {
      this.flagMapping[flag] = res;
    })
}

最后,根据 flag 传参我们改写 Worker 内部的 onmessage 函数以及返回结果函数的判断逻辑:

const onMessageHandlerFn = `self.onmessage = ({ data: { data, flag } }) => {
  console.log('Message received from main script');
  const {method} = data;
  if (data.data && method === 'format') {
    self.postMessage({
      data: formatFn(data.data),
      flag
    });
  }
  console.log('Posting message back to main script');
}`;

// ...

const handleResult = ({ data: { data, flag } }) => {
  const resolve = this.flagMapping[flag];
  
  if (resolve) {
    resolve(data);
    delete this.flagMapping[flag];
  }
}


5 / 总结

至此,一个可动态创建、可复用的 Web Worker 便写完了,大致骨架见附录,完整代码见 GIST https://gist.github.com/hijiangtao/22607ea9e5f4dfe504381a99d4134142

当然,本文还有很多内容没有涉及,比如创建 subworker、比如共享 worker 等等。在处理简单逻辑时,本文所述的 Web Worker 已够用,其他就留到下篇文章再去详细谈谈吧。

原文:https://hijiangtao.github.io/2019/03/21/Create-A-Dynamic-Web-Worker-With-Blob/

链接: https://fly63.com/article/detial/7475

Service Worker的生命周期

service worker的生命周期是它最复杂的部分。如果你不知道它在努力做什么和这么做的优势,你会感到它在跟你对着干。但一旦你知道了它的原理,你就可以给用户提供无缝的,优雅而不突兀的更新。一种同时具备网站应用和原生应用优势的体验。

Web Worker 详细介绍_Web Workers的使用

web worker 是运行在后台的 JavaScript,独立于其他脚本,也就是说在Javascript单线程执行的基础上,开启一个子线程,进行程序处理,而不影响主线程的执行。Service Worker 是一个由事件驱动的 worker,它由源和路径组成,以加载 .js 文件的方式实现的。

web worker是什么?理解并使用web worker

Web Worker 是为了解决 JavaScript 在浏览器环境中没有多线程的问题。正常形况下,浏览器执行某段程序的时候会阻塞直到运行结束后在恢复到正常状态,而HTML5的Web Worker就是为了解决这个问题,提升程序的执行效率。

Web Worker模拟选票

思路:五个人(5个div窗口模拟)同时进行抢票,有百分之十的几率可以抢到票,抢到票后对应的窗口(即随机生成的数大于等于0小于9的情况)会编程天蓝色,没抢到票的窗口(即随机生成的数大于9小于100的情况)会变成红色

Node.js中的Worker Threads

想要明白workers,首先需要明白node是怎样构成的。当一个node进程开始,它其实是:一个进程:是指一个全局对象,这个对象能够访问任何地方,并且包含当前处理时的此时信息。

关于Web Workers你需要了解的7件事

Web Workers允许你在后台运行JavaScript代码,而不会阻止web用户界面。Web Workers可以提高网页的整体性能,还可以增强用户体验。Web Workers有两种风格 ——专用Web Workers和共享Web Workers

如何使用JS中的webWorker?

作为浏览器脚本语言,如果JavaScript不是单线程,那么就有点棘手了。比如,与用户交互或者对DOM进行操作时,在一个线程上修改某个DOM,另外的线程删除DOM,这时浏览器该如何抉择呢?

Web Worker 现状

Web 是单线程的。这让编写流畅又灵敏的应用程序变得越来越困难。Web Worker 的名声很臭,但对 Web 开发者来说,它是解决流畅度问题的 一个非常重要的工具。让我们来了解一下 Web Worker 吧

浅谈 HTML5 Web Worker,性能优化利器?

多线程是现代软件开发中用于增强应用的性能和响应能力的重要技术。然而,JavaScript 是一门单线程语言,它天生是不支持多线程的。为了克服这一限制,引入了 Web Workers。本文就来探讨 Web Workers 对 Web 多线程的重要性,以及使用它们的限制和注意事项。

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!