Web Workers的实时处理

2020年12月30日11:18:46 发表评论 33 次浏览

本文概述

作为JavaScript开发人员, 你应该已经了解它单线程处理模型:你的所有JavaScript代码都在一个线程中执行。甚至事件处理和异步回调都在同一线程中执行, 并且多个事件依次处理, 一个接一个。换句话说, 普通JavaScript代码的执行没有并行性。

这听起来可能很奇怪, 因为这意味着JavaScript代码没有充分利用计算机的计算能力。另外, 当代码块运行时间太长时, 此模型可能会引起一些问题。在这种情况下, 你的应用程序可能无法响应。

幸运的是, 最近的Web浏览器提供了一种解决此潜在性能问题的方法。 HTML5规范介绍网络工作者在浏览器端提供JavaScript计算的并行性。

在本文中, 我们将说明如何使用Web Workers。我们将构建一个简单的文本分析器并逐步增强其实现, 以避免由于JavaScript单线程处理模型而导致的性能问题。

构建一个实时文本分析器

我们的目标是实现一个简单的应用程序, 该应用程序在用户在文本区域中键入文本时显示一些有关文本的统计数据。

该应用程序的HTML标记如下所示:

<textarea id="text" rows="10" cols="150" placeholder="Start writing...">
</textarea>

<div>
  <p>Word count: <span id="wordCount">0</span></p>
  <p>Character count: <span id="charCount">0</span></p>
  <p>Line count: <span id="lineCount">0</span></p>
  <p>Most repeated word: <span id="mostRepeatedWord"></span> (<span id="mostRepeatedWordCount">0</span> occurrences)</p>
</div>

你可以看到一个文本区域元素(用户可以在其中写他们的文本)和div元素(应用程序在其中显示有关插入的文本的统计数据, 例如字数, 字符, 行和重复次数最多的字)。请记住, 这些数据是在用户书写时实时显示的。

相关的JavaScript代码提取和显示统计数据如下所示:

const text = document.getElementById("text");
const wordCount = document.getElementById("wordCount");
const charCount = document.getElementById("charCount");
const lineCount = document.getElementById("lineCount");
const mostRepeatedWord = document.getElementById("mostRepeatedWord");
const mostRepeatedWordCount = document.getElementById("mostRepeatedWordCount");

text.addEventListener("keyup", ()=> {
  const currentText = text.value;
  
  wordCount.innerText = countWords(currentText);
  charCount.innerText = countChars(currentText);
  lineCount.innerText = countLines(currentText);
  let mostRepeatedWordInfo = findMostRepeatedWord(currentText);
  mostRepeatedWord.innerText = mostRepeatedWordInfo.mostRepeatedWord;
  mostRepeatedWordCount.innerText = mostRepeatedWordInfo.mostRepeatedWordCount;
});

在这里, 你可以看到一个语句块, 该语句获取显示数据所涉及的各种DOM元素, 以及当用户完成按下每个键时捕获该数据的事件侦听器。

内部的键控通过事件侦听器, 你可以找到对执行实际数据分析的函数的一些调用:countWords(), countChars(), countLines()和findMostRepeatedWord()。你可以在以下位置找到这些功能的实现以及文本分析器的整个实现密码笔.

我们为制作了一个自定义演示.
不完全是。点击这里查看.

Web Workers的实时处理1

单线程性能问题

通过分析此简单文本分析器应用程序的源代码, 你可以看到, 每当用户完成按键盘上的某个键时, 便会执行统计提取。当然, 与数据提取有关的计算工作量取决于文本的长度, 因此, 随着文本大小的增加, 你可能会失去性能。

考虑到此示例考虑的文本分析功能非常简单, 但是你可能希望提取更复杂的数据, 例如关键字及其相关性, 单词分类, 平均句子长度等。即使使用短文本或中等长度的文本, 该应用程序也可能会表现良好, 但是你可能会遇到性能下降的情况, 并使该应用程序对长文本失去响应, 尤其是在低性能的设备(例如手机。

网络工作者基础

单线程处理模型是JavaScript语言规范它将同时应用于浏览器和服务器。为了克服这种语言限制, HTML5规范介绍了工人概念, 即提供在单独线程中执行JavaScript代码的方法的对象。

创建工作程序非常简单:你所要做的就是将要执行的代码隔离在文件的单独线程中, 并通过调用以下代码创建工作程序对象:工人()构造函数, 如以下示例所示:

const myWorker = new Worker("myWorkerCode.js");

这种工作程序称为Web工作程序(另一种工作程序是Service工作程序, 但不在本文讨论范围之内)。

主线程和工作线程之间的交互基于消息交换系统。主线程和工作线程都可以使用postMessage()方法发送消息, 并通过处理message事件来接收消息。

例如, 主线程可以通过发送如下消息来启动工作线程:

myWorker.postMessage("start");

如你所见, 我们通过了开始>字符串作为参数postMessage(), 但你可以传递任何内容。这取决于你以及Web Worker的期望, 但是请记住, 你无法传递函数。但是请记住, 数据是按值传递的。因此, 如果你传递一个对象, 它将是克隆的工人对其进行的任何更改都不会影响原始对象。

工作人员通过实现以下内容的侦听器来接收消息:信息事件, 如下所示:

self.addEventListener("message", (event) => {
  if (event.data === "start") {
    //do things
  }
});

你会注意到self关键字。它指的是当前工作程序上下文, 与主线程的全局上下文不同。你也可以使用这个关键字表示工作人员上下文, 但按照惯例, 通常首选self。

因此, 在上面的示例中, 你将事件侦听器附加到当前工作程序上下文, 并通过event.data属性访问来自主线程的数据。

以相同的方式, 工作程序可以使用postMessage()将消息发送到主线程:

self.postMessage("ok");

并且主线程通过处理message事件接收消息, 如下所示:

myWorker.addEventListener("message", (event) => {
  if (event.data === "ok") {
    //do things
  }
});

请注意, 一个工作程序可以创建另一个工作程序并与其进行通信, 因此交互不限于工作程序和主线程。

最后, 你可以通过两种方式显式停止工作进程:通过调用self.close()从工作进程内部进行调用, 以及通过使用terminate()方法从调用线程进行停止, 如以下示例所示:

myWorker.terminate();

文本分析器的Web Worker

在探究了Web Workers的基础之后, 让我们将其应用于我们的应用程序。

首先, 让我们提取代码以放入一个名为textAnalyzer.js的单独文件中。你可以借此机会通过定义函数analytics()并返回文本分析的结果来重构代码, 如下所示:

function analyze(str) {
  const mostRepeatedWordInfo = findMostRepeatedWord(str);
  
  return {
    wordCount: countWords(str), charCount: countChars(str), lineCount: countLines(str), mostRepeatedWord: mostRepeatedWordInfo.mostRepeatedWord, mostRepeatedWordCount: mostRepeatedWordInfo.mostRepeatedWordCount
  };
}

其他功能countWords(), countChars()等在同一textAnalyzer.js文件中定义。

在同一文件中, 我们需要处理message事件, 以便与主线程进行交互。以下是所需的代码:

self.addEventListener("message", (event) => {
  postMessage(analyze(event.data));
});

事件侦听器期望在事件对象的data属性中分析文本。它的唯一任务是简单地通过postMessage()返回对文本应用analytics()函数的结果。

现在, 主脚本中的JavaScript代码如下:

const text = document.getElementById("text");
const wordCount = document.getElementById("wordCount");
const charCount = document.getElementById("charCount");
const lineCount = document.getElementById("lineCount");
const mostRepeatedWord = document.getElementById("mostRepeatedWord");
const mostRepeatedWordCount = document.getElementById("mostRepeatedWordCount");

const textAnalyzer = new Worker("textAnalyzer.js");

text.addEventListener("keyup", ()=> {
  textAnalyzer.postMessage(text.value);  
});

textAnalyzer.addEventListener("message", (event) => {
  const textData = event.data;
  
  wordCount.innerText = textData.wordCount;
  charCount.innerText = textData.charCount;
  lineCount.innerText = textData.lineCount;
  mostRepeatedWord.innerText = textData.mostRepeatedWord;
  mostRepeatedWordCount.innerText = textData.mostRepeatedWordCount;
});

如你所见, 我们创建了textAnalyzer基于textAnalyzer.js文件的Web Worker。

每次用户输入密钥时, 都会通过带有完整文本的postMessage()向工作人员发送一条消息。工作者的响应来自对象形式的event.data, 其属性值分配给各个DOM元素进行显示。

由于Web Worker的代码是在单独的线程中执行的, 因此用户可以在进行文本分析时继续插入新文本, 而不会出现无响应的情况。

处理错误

如果在工作程序执行期间发生错误, 会发生什么情况?在这种情况下, 将引发错误事件, 你应该通过常规事件侦听器在调用线程中对其进行处理。

例如, 假设我们的文本分析器工作程序检查消息中传递的数据是否实际上是文本, 如以下代码所示:

self.addEventListener("message", (event) => {
  if (typeof event.data === "string") {
    postMessage(analyze(event.data));    
  } else {
    throw new Error("Unable to analyze non-string data");
  }
});

侦听器在分析传递的数据并将消息发送到主线程之前, 确保传递的数据是字符串。如果传递的数据不是文本, 则引发异常。

在主线程方面, 应通过为错误事件实现侦听器来处理此异常, 如下所示:

textAnalyzer.addEventListener("error", (error) => {
  console.log(`Error "${error.message}" occurred in the file ${error.filename} at line ${error.lineno}`);
});

事件处理程序会收到一个错误对象, 其中包含一些有关发生问题的数据。在示例中, 我们使用了:

  • 的信息属性描述发生的错误,
  • 的文档名称属性报告实现工作程序的脚本文件的名称
  • 的Lineno属性包含发生错误的行号

你可以通过以下方法找到该实现的完整代码这个连结.

网络工作者限制

我希望你同意Web Workers令人惊奇并且使用非常简单:你只需要使用普通的JavaScript和标准事件处理来实现线程之间的互操作。没有什么特别奇怪或复杂的。

但是, 请记住, Web Workers有一些限制:

  • 他们不能访问DOM窗口或者文件对象。因此, 例如, 请勿尝试使用console.log()在浏览器的控制台上打印消息。为了使Web Workers线程安全, 此限制以及传递序列化的消息数据是必需的。乍一看似乎过于严格, 但实际上, 这种限制将引导你更好地分离问题, 一旦你学会了如何与工人打交道, 好处就会显而易见。
  • 此外, 仅当通过HTTP或HTTPS协议提供应用程序文件时, Web Worker才运行。换句话说, 如果你的网页是通过本地文件系统加载的, 则它们不会运行文件://协议。
  • 最后, 相同的来源策略也适用于Web Workers。这意味着实现工作程序的脚本必须与调用脚本从相同的域提供服务, 包括协议和端口。

共享工作者

如前所述, Web Workers用于执行昂贵的处理任务, 以分配计算负荷。有时, Web Worker可能需要大量资源, 例如内存或本地存储。当打开来自同一应用程序的多个页面或框架时, 这些资源对于Web Worker的每个实例都是重复的。如果你的工作程序逻辑允许, 则可以通过在多个浏览器上下文之间共享Web工作程序来避免增加资源请求。

共享的工人可以帮助你。它们是到目前为止我们看到的Web Workers的变体。为了将此变体类型与先前的变体类型区分开, 通常将后者称为敬业的工人.

让我们来看看如何通过转换文本分析器来创建共享工作器。

第一步是使用SharedWorker()构造函数代替工人():

const textAnalyzer = new SharedWorker("textAnalyzer.js");

该构造函数为工作人员创建代理。由于工作人员将与多个呼叫者进行通信, 因此代理将具有专用端口, 必须使用该端口来连接侦听器和发送消息。因此, 你需要为消息事件附加侦听器, 如下所示:

textAnalyzer.port.addEventListener("message", (event) => {
  const textData = event.data;
  
  wordCount.innerText = textData.wordCount;
  charCount.innerText = textData.charCount;
  lineCount.innerText = textData.lineCount;
  mostRepeatedWord.innerText = textData.mostRepeatedWord;
  mostRepeatedWordCount.innerText = textData.mostRepeatedWordCount;
});

请注意, 唯一的区别是使用端口属性附加了事件侦听器。同样, 你需要使用port属性通过postMessage()发送消息:

text.addEventListener("keyup", ()=> {
  textAnalyzer.port.postMessage(text.value);
});

但是, 与以前不同, 你需要通过调用start()方法将线程显式连接到工作线程, 如下所示:

textAnalyzer.port.start();

这是确保端口在添加侦听器之前不会调度事件的必要条件。但是请记住, 你不需要调用start()如果你将听众附加到消息属性, 而不是使用addEventListener(), 如下所示:

textAnalyzer.port.onmessage = (event) => {
  const textData = event.data;
  
  wordCount.innerText = textData.wordCount;
  charCount.innerText = textData.charCount;
  lineCount.innerText = textData.lineCount;
  mostRepeatedWord.innerText = textData.mostRepeatedWord;
  mostRepeatedWordCount.innerText = textData.mostRepeatedWordCount;
};

在工作程序方面, 你需要通过使用以下代码替换消息事件侦听器来安排一些工作程序设置:

self.addEventListener("connect", (event) => {
  const port = event.ports[0];

  port.addEventListener("message", (event) => {
    if (typeof event.data === "string") {
      port.postMessage(analyze(event.data));    
    } else {
      throw new Error("Unable to analyze non-string data");
    }
  });

  port.start();
});

你为connect事件添加了一个侦听器。当调用方调用辅助代理端口的start()方法或将事件侦听器附加到事件代理时, 就会触发此事件消息属性。在这两种情况下, 都将端口分配给工作程序, 你可以通过访问事件对象的端口数组的第一个元素来获取它。与呼叫者类似, 你需要使用此端口来附加事件侦听器和发送消息。另外, 如果使用addEventListener()附加侦听器, 则需要通过port.start()方法与调用者建立连接。

现在, 你的工作者已成为共享工作者。

有关此实现的完整代码, 请参见

这个连结

.

总结

在本文中, 我们讨论了JavaScript单线程处理模型在某些情况下可能存在的局限性。一个简单的实时文本分析器的实现试图更好地解释该问题。

引入了Web Worker来解决潜在的性能问题。它们用于在单独的线程中生成。我们讨论了Web Workers的限制, 最后解释了当我们需要在多个页面或框架之间共享Web Worker时如何创建共享Worker。

你可以在以下位置找到本文中创建的工作人员的最终代码。该GitHub存储库。

日志火箭:全面了解你的网络应用

LogRocket仪表板免费试用横幅

日志火箭是一个前端应用程序监视解决方案, 可让你重播问题, 就好像问题发生在你自己的浏览器中一样。 notlogy无需猜测错误发生的原因, 也不要求用户提供屏幕截图和日志转储, 而是让你重播会话以快速了解出了什么问题。无论框架如何, 它都能与任何应用完美配合, 并具有用于记录来自Redux, Vuex和@ ngrx / store的其他上下文的插件。

除了记录Redux动作和状态外, notlogy还会记录控制台日志, JavaScript错误, 堆栈跟踪, 带有标题+正文, 浏览器元数据和自定义日志的网络请求/响应。它还使用DOM来记录页面上的HTML和CSS, 甚至可以为最复杂的单页面应用程序重新创建像素完美的视频。

免费试用

.

一盏木

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: