这是探索 JavaScript 及其内建组件系列文章的第 7 篇。在认识和描述这些核心元素的过程当中,咱们也会分享咱们在构建 SessionStack 时所遵循的一些经验规则。SessionStack 是一个轻量级 JavaScript 应用,它协助用户实时查看和复现他们的 Web 应用缺陷,所以其自身不只须要足够健壮还要有不俗的性能表现。javascript
若是你错过了前面的文章,你能够在下面找到它们:html
这一次咱们将剖析 Web Worker:对它进行简单概述后,咱们将分别讨论不一样类型的 Worker 以及它们内部组件的运做方法,同时也会以场景为例说明它们各自的优缺点。在文章的最后,咱们将讲解最适合使用 Web Worker 的 5 个场景。前端
咱们在 以前的文章 中已经详尽地讨论了 JavaScript 的单线程运行机制,对此你应当已经了然于胸。然而,JavaScript 是容许开发者在单线程模型上书写异步代码的。html5
咱们已经讨论过了 异步编程 的概念及其使用场景。java
异步编程经过把部分代码 “放置” 到事件循环较后的时间点执行,保证了 UI 渲染始终处于较高的优先级,这样你的 UI 就不会出现卡顿无响应的状况。android
AJAX 请求是异步编程的最佳实践之一。一般网络请求不会在短期内获得响应,所以异步的网络请求能让客户端在等待响应结果的同时执行其余业务代码。ios
// 假设你使用了 jQuery
jQuery.ajax({
url: 'https://api.example.com/endpoint',
success: function(response) {
// 正确响应后须要执行的代码
}
});
复制代码
固然这里有个问题,上例可以进行异步请求是依靠了浏览器提供的 API,其余代码又该如何实现异步执行呢?例如,在上例 success 回调函数中存在 CPU 密集型计算:git
var result = performCPUIntensiveCalculation();
复制代码
假如 performCPUIntensiveCalculation
不是一个 HTTP 请求,而是一段能够阻塞线程的代码(例:一段巨型 for
循环代码)。这样会使 event loop 不堪重负,浏览器 UI 也随之阻塞 —— 用户将面对卡顿无响应的网页。github
这就说明了使用异步函数只能解决 JavaScript 单线程模型带来的一小部分问题。web
在一些因大量计算引发的 UI 阻塞问题中,使用 setTimeout
来解决阻塞的效果还不错。例如,咱们能够把一系列的复杂计算分批放到单独的 setTimeout
中执行,这样作等因而把连续的计算分散到了 event loop 中的不一样位置,以此为 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) {
// 把下一次函数调用放入 event loop
setTimeout(function() {
sum += numbers[i];
calculateSumAsync(i + 1);
}, 0);
} else {
// 计算完数组中全部元素后,调用回调函数返回结果
callback(sum / len);
}
}
calculateSumAsync(0);
}
复制代码
经过使用 setTimeout
能够把每一步计算都放置到 event loop 较后的时间点执行。在每两次的计算间隔,event loop 便会有足够的时间执行其余计算,从而保证浏览器不会一 ”冻“ 不动。
HTML5 已经提供了很多开箱即用的好东西,包括:
Web Worker 是内建在浏览器中的轻量级 线程,使用它执行 JavaScript 代码不会阻塞 event loop。
很是神奇吧,原本 JavaScript 中的全部范例都是基于单线程模型实现的,但这里的 Web Worker 却(在必定程度上)突破了这一限制。
今后开发者能够远离 UI 阻塞的困扰,经过把一些执行时间长、计算密集型的任务放到后台交由 Web Worker 完成,使他们的应用响应变得更加迅速。更重要的是,咱们不再须要对 event loop 施加任何的 setTimeout
黑魔法。
这里有一个简单的数组排序 demo ,其中对比了使用 Web Worker 和不使用 Web Worker 时的区别。
Web Worker 容许你在执行大量计算密集型任务时,还不阻塞 UI 进程。事实上,两者互不不阻塞的缘由就是它们是并行执行的,能够看出 Web Worker 是货真价实的多线程。
你可能想说 — ”JavaScript 不是一个在单线程上执行的语言吗?“。
你可能会惊讶 JavaScript 做为一门编程语言,却没有定义任何的线程模型。所以 Web Worker 并不属于 JavaScript 语言的一部分,它仅仅是浏览器提供的一项特性,只是它能够被 JavaScript 访问、调用罢了。过往的众多浏览器都是单线程程序(之前的理所固然,如今也有了些许变化),而且浏览器一直以来也是 JavaScript 主要的运行环境。对比在 Node.JS 中就没有 Web Worker 的相关实现 — 虽然 Web Worker 对应着 Node.JS 中的 “cluster” 或 “child_process” 概念,不过它们仍是有所区别的。
值得注意的是,Web Worker 的 定义 中一共包含了 3 种类型的 Worker:
Dedicated Worker 由主线程实例化且只能与它通讯。
Dedicated Worker 浏览器兼容性一览
Shared Worker 能够被同一域(浏览器中不一样的 tab、iframe 或其余 Shared Worker)下的全部线程访问。
Shared Worker 浏览器兼容一览
Service Worker 是一个事件驱动型 Worker,它的初始化注册须要网页/站点的 origin 和路径信息。一个注册好的 Service Worker 能够控制相关网页/网站的导航、资源请求以及进行粒度化的资源缓存操做,所以你能够极好地控制应用在特定环境下的表现(如:无网络可用时)。
Service Worker 浏览器兼容一览
在本文中,咱们主要讨论 Dedicated Worker,后文的 ”Web Worker“ 或 “Worker” 都默认指代它。
最终实现 Web Worker 的是一堆 .js
文件,网页会经过异步 HTTP 请求来加载它们。固然 Web Worker API 已经包办了这一切,上述加载对使用者彻底无感。
Worker 利用相似线程的消息机制保持了与主线程的平行,它是提高你应用 UI 体验的不二人选,使用 Worker 保证了 UI 渲染的实时性、高性能和快速响应。
Web Worker 是运行在浏览器内部的一条独立线程,所以须要使用 Web Worker 运行的代码块也必须存放在一个 独立文件 中。这一点须要牢记在心。
让咱们看看,如何建立一个基础 Worker:
var worker = new Worker('task.js');
复制代码
若是此处的 “task.js” 存在且能被访问,那么浏览器会建立一个新的线程去异步地下载源代码文件。一旦下载完成,代码将马上执行,此时 Worker 也就开始了它的工做。 若是提供的代码文件不存在返回 404,那么 Worker 会静默失败并不抛出异常。
为了启动建立好的 Worker,你须要显式地调用 postMessage
方法:
worker.postMessage();
复制代码
为了使建立好的 Worker 和建立它的页面可以通讯,你须要使用 postMessage
方法或 Broadcast Channel(广播通道).
在较新的浏览器中,postMessage 方法支持 JSON
对象做为函数的第一个入参,可是在旧版本浏览器中它仍是只支持 string
。
下面的 demo 会展现 Worker 是如何与建立它的页面进行通讯的,同时咱们将使用 JSON 对象做为通讯体好让这个 demo 看起来稍微 “复杂” 一点。若改成传递字符串,方法也不言而喻了。
让咱们看看下面的 HTML 页面(或者准确地说是片断):
<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 脚本中的内容:
self.addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'average':
var result = calculateAverage(data); // 一个计算数值型数组元素均值的函数
self.postMessage(result);
break;
default:
self.postMessage('Unknown command');
}
}, false);
复制代码
当主页面中的 button 被按下,触发调用了 postMessage
方法。worker.postMessage
这行代码会传递一个 JSON
对象给 Worker,对象中包含了 cmd
和 data
两个键以及它们对应的值。相应的,Worker 会经过定义的 message
响应方法拿到和处理上面传递过来的消息内容。
当消息到达 Worker 后,实际的计算便开始运行,这样彻底不会阻塞 event loop。在此过程当中,Worker 只会检查传递来的事件 e
,而后像往常执行 JavaScript 函数同样继续执行。当最终执行完成,执行结果会回传回主页面。
在 Worker 的执行上下文中,self
和 this
都指向 Worker 的全局做用域。
有两种中止 Worker 的方法:一、在主页面中显示地调用
worker.terminate()
;二、在脚本中调用self.close()
让 Worker 自行了断。
Broadcast Channel 是更纯粹地为通讯而生的 API。它容许咱们在同域下的全部的上下文中发送和接收消息,包括浏览器 tab、iframe 和 Worker:
// 建立一个到 Broadcast Channel 的链接
var bc = new BroadcastChannel('test_channel');
// 发送一段简单的消息
bc.postMessage('This is a test message.');
// 这是一个简单的事件 handler
// 咱们会在 handler 中接收并打印消息到终端
bc.onmessage = function (e) {
console.log(e.data);
}
// 断开与 Broadcast Channel 的链接
bc.close()
复制代码
下图会帮助你理解 Broadcast Channel 的工做原理:
使用 Broadcast Channel 会有更严格的浏览器兼容限制:
一共有 2 种给 Web Worker 发送消息的方法:
由于 Web Worker 的多线程天性使然,它只能使用 一小撮 JavaScript 提供的特性,列表以下:
navigator
对象location
对象(只读)XMLHttpRequest
setTimeout()/clearTimeout()
与 setInterval()/clearInterval()
importScripts()
引入外部 script使人遗憾的是 Web Worker 没法访问一些很是重要的 JavaScript 特性:
window
对象document
对象parent
对象这意味着 Web Worker 不能作任何的 DOM 操做(也就是 UI 层面的工做)。刚开始这会显得略微棘手,不过一旦你学会了如何正确使用 Web Worker。你就只会把 Web Worker 用做单独的 ”计算机器“,而把全部的 UI 操做放到页面代码中。你能够把全部的脏活累活都交给 Web Worker 完成,再将它劳做的结果传到页面并在那里进行必要的 UI 操做。
像对待任何 JavaScript 代码同样,你但愿处理 Web Worker 抛出的任何错误。当 Worker 在运行时发生错误,它会触发 ErrorEvent
事件。该接口包含 3 个有用的属性,它们能帮助你定位代码出错的缘由:
这有一个例子:
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(); // 不传递消息仅启动 Worker
复制代码
self.addEventListener('message', function(e) {
postMessage(x * 2); // 此行故意使用了未声明的变量 'x'
};
复制代码
能够看到,咱们在这儿建立了一个 Worker 并监听着它发出的 error
事件。
经过使用一个在做用域内未定义的变量 x
做乘法,咱们在 Worker 内部(workerWithError.js
文件内)故意制造了一个异常。这个异常会被传递到最初建立 Worker 的 scrpit 中,同时调用 onError
函数。
到此为止咱们已经见识了 Web Worker 的强悍与不足,下面就一块儿来看看最适合使用它的场景有哪些:
光线追踪(Ray Tracing)::光线追踪属于计算机图形学中的 渲染(Rendering) 技术,它会追踪并转换光线 的轨迹为一个个像素点,最终生成一张完整的图片。为模拟光线的轨迹,光线追踪须要 CPU 进行大量的数学计算。光线追踪包括模拟光的反射、折射及物质效果等。以上全部的计算逻辑均可以交给 Web Worker 完成,从而不阻塞 UI 线程的执行。或者更好的方案是使用多个 Worker (以及多个 CPU)来完成图片渲染。这有一个使用 Web Worker 进行光线追踪的 demo — nerget.com/rayjs-mt/ra….
加密: 针对我的敏感数据的保护条例变得日益严格,端对端的数据加密也变得更为流行。当程序中须要常常加密大量数据时(如向服务器发送数据),加密成为了很是耗时的工做。Web Worker 能够很是好的切入此类场景,由于这里不涉及任何的 DOM 操做,Worker 中仅仅运行一些专为加密的算法。Worker 会勤恳地默默工做,丝绝不会打扰用户,也毫不会影响用户的体验。
数据预获取: 为优化你的网站或 web 应用的数据加载时长,你可使用 Web Worker 预先获取一些数据,存储起来以备后续使用。Web Worker 在这里发挥着重要做用,由于它毫不会影响应用的 UI 体验,若不使用 Web Worker 状况会变得异常糟糕。
Progressive Web App: 当网络状态不是很理想时,你仍需保证 PWA 有较快的加载速度。这就意味着 PWA 的数据须要被持久化到本地浏览器中。在此背景下,一些与 IndexDB 相似的 API 便应运而生了。从根本上来讲,客户端一侧须要有数据存储能力。为保证存取时不阻塞 UI 线程,这部分工做理应交给 Web Worker 完成。好吧,在 IndexDB 中你能够不使用 Web Worker,由于它提供的异步 API 一样不会阻塞 UI。可是在这以前,IndexDB 提供的是同步API(可能会被再次引入),这种状况使用 Web Worker 仍是很是有必要的。
拼写检查: 进行拼写检查的基本流程以下 — 程序首先从词典文件中读取一系列拼写正确的单词。整个词典的单词会被解析为一个搜索树用于实际的文本搜索。当待测词语被输入后,程序会检查已创建的搜索树中是否存在该词。若是在搜索树中没有匹配到待测词语,程序会替换字符组成新的词语,并测试新的词语是不是用户期待输入的,若是是则会返回该词语。整个检测过程能够被轻松 “下放” 给 Web Worker 完成,Worker 会完成全部的词语检索和词语联想工做,这样一来用户的输入就不会阻塞 UI 了。
对 SessionStack 来讲,保持高性能和高可靠性是极其重要的. 持有这种理念的主要缘由是,一旦你的应用集成 SessionStack 后,它会开始记录从 DOM 变化、用户交互行为到网络请求、未捕获异常和 debug 信息的全部数据。收集到的跟踪数据会被 实时 发送到后台服务器,以视频的形式向你还原应用中出现的问题,帮助你从用户的角度重现错误现场。这一切功能的实现须要足够的快而且不能给你的应用带来任何性能上的负担。
这就是为何咱们尽量地把 SessionStack 中,值得优化的业务逻辑交给 Web Worker 完成。诸如在核心监控库和播放器中,都包含了像 hash 数据完整性验证、渲染等 CPU 密集型任务,这些都是值得使用 Web Worker 优化的地方。
Web 技术持续向前变动和发展,因此咱们宁可先行一步也要保证 SessionStack 是一个不会给用户 app 带来任何性能损耗的轻量级应用。
若是阁下愿意试试 SessionStack ,这里有一个免费的试用计划。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。