Web Worker 的构成元素 + 5个使用场景
潘忠显 / 2021-04-07
“JavaScript 工作原理”系列文章是翻译和整理自 SessionStack 网站的 How JavaScript works。因为博文发表于2017年,部分技术或信息可能已经过时。本文英文原文链接,作者 Alexander Zlatkov,翻译 潘忠显。
这是致力于探索 JavaScript 及其构建组件的系列文章的第7部分,本文将拆解分析 Web Workers:首先是概述,然后讨论不同类型的Workers,它们的建筑组件如何一起发挥作用,以及它们在不同情况下提供的优势和局限性。最后,我们将提供5个用例,其中Web Workers将是正确的选择。
This time we’ll be taking apart Web Workers: we’ll offer an overview, discuss the different types of workers, how their building components come to play together, and what advantages and limitations they offer in different scenarios. Finally, we’ll provide 5 use cases in which Web Workers will be the right choice.
[TOC]
您应该已经熟悉 JavaScript 在单个线程上运行这一事实,正如我们之前详细讨论过的那样。但是,JavaScript 也为开发人员提供了编写异步代码的机会。
异步编程的局限性
我们已经讨论了异步编程以及它的使用场景。异步编程能够“调度”部分代码在事件循环中稍后执行,通过这种方式使应用程序 UI 能够作响应,从而允许首先执行 UI 渲染。
异步编程的一个很好的用例是发出 AJAX 请求。由于请求可能要花费大量时间,因此在异步发出请求、等待响应时,客户端可以执行其他代码。
// This is assuming that you're using jQuery
jQuery.ajax({
url: 'https://api.example.com/endpoint',
success: function(response) {
// Code to be executed when a response arrives.
}
});
但是,这带来了一个问题:请求由浏览器的 WEB API 处理,但是如何使其他代码异步?例如,如果成功回调中的代码占用大量 CPU 资源,该怎么办:
var result = performCPUIntensiveCalculation();
如果 performCPUIntensiveCalculation
不是HTTP请求,而是阻塞代码(例如,巨大的 for
循环),则无法释放事件循环,也无法解除对浏览器的用户界面的阻塞。相反,用户感受到的是,浏览器冻结且无法响应。这意味着异步函数仅解决 JavaScript 语言的单线程的一小部分限制。
在某些情况下,使用 setTimeout
可以使 UI 不受长时间运行的计算的阻塞。例如,通过将负责的计算拆解成批量独立的 setTimeout
调用,可以将拆分的计算放在单独的“位置”,这种方式可以为 UI 的渲染和响应争取到时间。
让我们看一个简单的函数,该函数可以计算数字数组的平均值:
function average(numbers) {
var len = numbers.length,
sum = 0,
i;
if (len === 0) {
return 0;
}
for (i = 0; i < len; i++) {
sum += numbers[i];
}
return sum / len;
}
可以将上面的代码以“模拟”异步性的方式进行重写:
function averageAsync(numbers, callback) {
var len = numbers.length,
sum = 0;
if (len === 0) {
return 0;
}
function calculateSumAsync(i) {
if (i < len) {
// Put the next function call on the event loop.
setTimeout(function() {
sum += numbers[i];
calculateSumAsync(i + 1);
}, 0);
} else {
// The end of the array is reached so we're invoking the callback.
callback(sum / len);
}
}
calculateSumAsync(0);
}
上边代码用到的 setTimeout
函数,会将每一步计算添加到事件循环。在每次计算之间,将会有足够的时间进行其他计算,而这是解冻浏览器所必须的。
Web Worker 可以节省时间
HTML5 为我们带来了很多很棒的东西,包括:
- SSE(我们已经在[上一篇文章]()中对其进行了描述,并与 WebSockets 进行了比较)
- 地理位置 (Geolocation)
- 应用程序缓存 (Application cache)
- 本地存储 (Local Storage)
- 拖放 (Drag and Drop)
- WebWorker
Web Worker 是浏览器中的“线程”,可用于执行 JavaScript 代码而不会阻塞事件循环。JavaScript 的整个范例都是基于单线程环境的思想,但 Web Worker 能够在一定程度上,消除单线程的限制。
Web Worker 允许开发这将长时间运行且计算量大的任务放在后台,而不会阻塞UI,从而使您的应用程序具有更高的响应速度。更重要的是,无需使用上面提到的setTimeout
技巧,就可以绕开事件循环。
这是一个简单的 demo,显示了使用Web Workers和不使用Web Workers对数组进行排序之间的区别。(译注:without web worker 会卡住页面,而with web worker 进度条会动)
Web Worker 概述
Web Workers 允许您执行一些操作而又不会阻塞 UI,这个操作可以是一个长时间运行、计算量大的脚本任务。实际上,Web Worker 是真正的多线程,这些操作都是并行进行的。
您可能会反问:“ JavaScript 不是单线程语言吗?”
JavaScript 是一种没有定义线程模型的语言,但 Web Worker 不是 JavaScript 的一部分,而是一种可通过J avaScript 访问的浏览器特性。
历史上,大多数浏览器曾经都是单线程的(当然,这点目前已经发生了变化),而且大多数 JavaScript 的实现都发生在浏览器当中。Web Workers 没有被 Node.js 实现:Node.js 中有“集群”或“子进程”的概念,这有点不同。
值得注意的是, 规范 提到了三种类型的Web Worker:
- 专用 worker,Dedicated Workers
- 共享 worker,Shared Workers
- 服务 worker,Service workers
专用 (dedicated) worker
专用的 Web Workers 由主进程实例化,且只能与主进程进行通信。
共享 (shared) worker
共享 worker 可以被相同来源 (origin) 的所有进程访问。这里的来源是指不同的浏览器标签、iframe或者其他共享worker(??different browser tabs, iframes or other shared workers)
服务 (service) worker
服务worker是事件驱动的、已针对原点和路径进行注册的 worker。它可以控制与之关联的网页/站点,拦截和修改导航和资源请求,以非常精细的方式缓存资源,从而使开发者可以控制应用在特定情况下的行为方式(例如,当网络不正常时)。
在本文中,我们将重点关注“专用工作者 (Dedicated Worker)”,并将其称为“Web Workers”或“Workers”。
Web Worker 的工作原理
Web Workers 被实现为 .js
文件,这些文件通过异步 HTTP 请求包含在页面中,而这些请求被 Web Worker API 完全地隐藏了。
Web Worker 利用类似线程的消息传递来实现并行,这非常适合使用户界面保持最新、高效且能及时响应用户。
Web Worker 在浏览器的单独隔离的线程中运行,因此,它们执行的代码需要包含在单独的文件中,这一点非常重要。
让我们看看如何创建一个基础的 worker:
var worker = new Worker('task.js');
如果 “task.js” 文件存在且可以访问,则浏览器将生成一个新线程,该线程异步下载该文件。下载完成后,将立即执行下载并开始工作。如果提供的文件路径返回 404
,则工作程序将静默失败。
为了启动已创建的 worker,您需要调用 postMessage
方法:
worker.postMessage();
Web Worker 间通信
为了在 Web Worker 和创建它的页面之间进行通信,您需要使用 postMessage
方法或广播频道 (Broadcast Channel)。
postMessage 方法
较新的浏览器支持将 JSON
对象作为第一个参数传入 postMessage
函数,而较旧的浏览器仅支持 string
。
传递字符串与传递 JSON 类似,但 JSON 更复杂一些。以下示例是部分 HTML 页面,将会展示创建 worker 的页面是如何通过传递 JSON 对象,与之进行来回通信的:
<button onclick="startComputation()">Start computation</button>
<script>
function startComputation() {
worker.postMessage({'cmd': 'average', 'data': [1, 2, 3, 4]});
}
var worker = new Worker('doWork.js');
worker.addEventListener('message', function(e) {
console.log(e.data);
}, false);
</script>
这就是我们的 worker 脚本 doWork.js 的样子:
self.addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'average':
var result = calculateAverage(data); // Some function that calculates the average from the numeric array.
self.postMessage(result);
break;
default:
self.postMessage('Unknown command');
}
}, false);
单击该按钮后,将从主页面调用 postMessage
。worker.postMessage
将 JSON
对象传递给 worker,并添加 cmd
和 data
键及其各自的值。worker 通过已定义的 message
处理程序处理该消息。
消息到达后,将在 worker 中执行实际计算,而不会阻塞事件循环。worker 检查传递的事件e
,并像标准的 JavaScript 函数一样执行,message
执行完成后,结果将传递回主页。
在一个工作者的上下文中,self
和 this
都引用了 worker 的全局范围。
有两种方法可以停止 worker:通过在主页上调用
worker.terminate()
或在 worker 本身内部调用self.close()
。
广播频道 (Broadcast Channel)
广播频道 (Broadcast Channel) 是一种更通用的通信 API,利用广播频道,我们可以将消息广播到共享相同来源的所有上下文。来自相同来源的所有浏览器标签、iframe、服务的 worker 可以发出和接收消息:
// Connection to a broadcast channel
var bc = new BroadcastChannel('test_channel');
// Example of sending of a simple message
bc.postMessage('This is a test message.');
// Example of a simple event handler that only
// logs the message to the console
bc.onmessage = function (e) {
console.log(e.data);
}
// Disconnect the channel
bc.close()
广播频道视觉化之后,看起来更加清晰:
然而,浏览器对广播频道的支持更为有限:
消息大小的影响
有两种向 Web Worker 发送消息的方法:
- 复制消息:message 被序列化、复制、发送,然后在另一端反序列化。页面和 worker 不共享相同的实例,因此最终结果是在每个传递都创建了一个副本。大多数浏览器通过自动对两端的值进行 JSON 编码/解码来实现此功能,而这些数据操作会增加消息传输的开销。消息越大,发送耗时越长。
- 传输消息:这意味着原始发件者一旦发送,就无法再使用它。数据传输几乎是瞬时的。限制条件是 ArrayBuffer 是可转让的。
Web Worker 可用的 JS 功能
由于其多线程性质,Web Workers 仅能访问部分 JavaScript 的功能。以下是功能列表:
navigator
对象location
对象(只读)XMLHttpRequest
setTimeout()/clearTimeout()
和setInterval()/clearInterval()
- 应用程序缓存
- 使用
importScripts()
导入外部脚本 - 创建其他 web worker
Web Worker的局限性
可悲的是,Web Workers 无法访问一些非常关键的 JavaScript 功能:
- DOM(不是线程安全的)
window
对象document
对象parent
对象
这意味着 Web Worker 无法操纵 DOM,因此也无法操纵UI,有时这可能会很棘手。但是一旦您了解了如何正确使用 Web Workers,您便会开始将它们用作单独的“计算机器”,而所有 UI 更改都将在您的页面代码中进行。worker 将为您完成计算繁重的任务,完成后会将结果传递给页面,该页面将对用户界面进行必要的更改。
错误处理
与任何 JavaScript 代码一样,您将需要处理 Web Worker 中抛出的所有错误。如果在执行 worker 时发生错误,则会触发 ErrorEvent
。该界面包含三个有用的属性,用于找到问题所在:
- filename:导致错误的 worker 脚本的名称
- lineno:发生错误的行号
- message:错误描述
这有一个例子:
function onError(e) {
console.log('Line: ' + e.lineno);
console.log('In: ' + e.filename);
console.log('Message: ' + e.message);
}
var worker = new Worker('workerWithError.js');
worker.addEventListener('error', onError, false);
worker.postMessage(); // Start worker without a message.
self.addEventListener('message', function(e) {
postMessage(x * 2); // Intentional error. 'x' is not defined.
};
您可以看到,我们在这里创建了一个 worker 并开始侦听 error
事件。
在 worker 脚本 workerWithError.js
中,该作用域内未定义 x
,我们通过将 x
乘以 2
来创建故意异常。异常会传播到初始脚本,并且会调用 onError
并将错误信息传递进去。
5 个 Web Workers 用例
我们已经列出了 Web Workers 的优点和缺点。接下来,让我们来看看有哪些最强的用例:
-
光线追踪:光线追踪是一种渲染 (rendering) 技术,通过以像素为单位跟踪光线 (light) 来产生图片,需要使用非常占用CPU的数学计算来模拟光的路径。这个想法是模拟一些效果,例如反射、折射、材质等。所有这些计算逻辑都可以添加到 Web Worker 中,以避免阻塞 UI 线程。更妙的是,可以轻松地将图像渲染划分为 worker,分别对应到多个CPU。这里是使用 Web Worker 进行光线跟踪的简单演示-。
-
加密:随着个人和敏感数据的相关法规越来越严格,端到端加密正变得越来越流行。加密可能会非常耗时,尤其很多数据需要频繁加密(例如,在将其发送到服务器之前)时。这是一个可以使用 Web Worker 很好的场景,因为它是纯算法来完成工作的,而不需要任何对 DOM 的访问或任何花哨的工作。进入 worker 后,它不会影响用户的体验。
-
预提取数据:为了优化您的网站或 Web 应用程序并缩短数据加载时间,您可以利用 Web Workers 预先加载和存储一些数据,以便以后在需要时使用它。与不使用 worker 不同的是,在这种情况下,Web Workers 它们不会影响您应用的用户界面。
-
渐进式 Web 应用程序:即使网络连接不稳定,它们也必须快速加载,这意味着必须将数据存储在本地浏览器中。这是 IndexDB 或类似 API 发挥作用的地方。基本上,为了在不阻塞UI线程的情况下使用它,须在 Web Worker 中完成客户端存储。就 IndexDB 而言,有一个异步 API 可以使没有使用 worker 也可以执行此操作,但是以前有一个同步 API 就只能在 worker 中使用。
-
拼写检查:基本的拼写检查器按以下方式工作:程序读取包含正确拼写单词列表的字典文件;将字典解析为搜索树,以提高实际文本的搜索效率;将单词提供给检查器,程序将检查单词是否存在于预构建的搜索树中;如果在树中找不到该单词,则可以通过替换字符并测试该单词是否为有效单词(如果该单词是用户要写的单词)来为用户提供其他拼写方式。所有这些处理都可以轻松地分担给 Web Worker,以便用户可以在不阻塞 UI 的情况下键入单词和句子,而 worker 执行所有搜索和提供建议的动作。