我的总结:javascript
1.webworkers实现了用多线程浏览器来进行多线程操做js的能力。html
2.web workers不能操做dom,window,document等对象,通常用于cpu计算型的任务。html5
这是 JavaScript 工做原理的第七章。java
如今,咱们将会剖析 Web Workers:咱们将会综合比较不一样类型的 workers,如何组合运用他们的构建模块来进行开发以及不一样场景下各自的优缺点。最后,咱们将会介绍 5 个 Web Workder 的使用场景。node
在前面的详细介绍的文章中你已经清楚地了解到 JavaScript 是单线程的事实。然而,JavaScript 也容许开发者编写异步代码。git
前面咱们了解到异步编程及其使用时机。github
异步编程经过调度部分代码使之在事件循环中延迟执秆,这样就容许优先渲染程序界面,从而让程序运行流畅。web
AJAX 请求是一个很好的异步编程的使用场景 。由于请求可能会花很长的时间,因此能够异步执行它们,而后在客户端等待数据返回的同时,运行其它代码。ajax
// 假设使用 jQuery jQuery.ajax({ url: 'https://api.example.com/endpoint', success: function(response) { // 当数据返回时候的代码 } });
然而,这里会产生一个问题-AJAX 请求是由浏览器网页 API 进行处理的,能够异步执行其它代码吗?好比,假设成功回调的代码是 CPU 密集型的:算法
var result = performCPUIntensiveCalculation();
若是 performCPUIntensiveCalculation
不是一个 HTTP 请求而是一个会阻塞界面渲染的代码(好比大量的 for
循环),这样就没有办法释放事件循环和浏览器的 UI-浏览器会被冻结住且失去响应。
这意味着,异步函数只是是解决了一部分 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) { // 在事件循环中调用下一个函数 setTimeout(function() { sum += numbers[i]; calculateSumAsync(i + 1); }, 0); } else { // 到达数组末尾,调用回调 callback(sum / len); } } calculateSumAsync(0); }
这里利用 setTimeout
函数在事件循环中循序添加每一次计算。在每一次计算之间,将会有充足的时间来进行其它的计算和解冻浏览器。
HTML5 给咱们带了不少开箱即用的好用的功能,包括:
Web Workers 是浏览器内置的线程因此能够被用来执行非阻塞事件循环的 JavaScript 代码。
屌爆了。整个 JavaScript 是基于单线程环境的而 Web Workers (部分)能够突破这方面的限制。
Web Workers 容许开发者把长时间运行和密集计算型的任务放在后台执行而不会阻塞 UI,这会使得应用程序运行得更加流畅。另外,这样就不用再使用 setTimeout
的黑科技来防止阻塞事件循环了。
这里有一个展现使用和未使用 Web Workers 来进行数组排序的区别的示例。
Web Workers 容许你作诸如运行处理 CPU 计算密集型任务的耗时脚本而不会阻塞 UI 的事情。事实上,全部这些操做都是并行执行的。Web Workers 是真正的多线程。
你或许会有疑问-『难道 JavaScript 不是单线程的吗?』。
当你意识到 JavaScript 是一门没有定义线程模型的语言的时候,或许你会感受很是的惊讶。Web Workers 并非 JavaScript 的一部分,他们是能够经过 JavaScript 进行操做的浏览器功能之一。之前,大多数的浏览器是单线程的(固然,如今已经变了),并且大多数的 JavaScript 功能是在浏览器端实现完成的。Node.js 没有实现 Web Workers -它有 『cluster』和 『child_process』的概念,这二者和 Web Workers 有些许差别。
值得注意的是,规范中有三种类型的 Web Workers:
Dedicated Web Workers 是由主进程实例化而且只能与之进行通讯
Dedicated Workers 浏览器支持状况
Shared workers 能够被运行在同源的全部进程访问(不一样的浏览的选项卡,内联框架及其它shared workers)。
Shared Workers 浏览器支持状况
Service Worker 是一个由事件驱动的 worker,它由源和路径组成。它能够控制它关联的网页,解释且修改导航,资源的请求,以及一种很是细粒度的方式来缓存资源以让你很是灵活地控制程序在某些状况下的行为(好比网络不可用)。
Service Workers 浏览器支持状况
本篇文章,咱们将会专一于 Dedicated Workers 并以 『Web Workers』或者 『Workers』来称呼它。
Web Workers 是以加载 .js
文件的方式实现的,这些文件会在页面中异步加载。这些请求会被 Web Worker API 彻底隐藏。
Workers 使用类线程的消息传输-获取模式。它们很是适合于为用户提供最新的 UI ,高性能及流畅的体验。
Web Workers 运行于浏览器的一个隔离线程之中。所以,他们所执行的代码必须被包含在一个单独的文件之中。请谨记这一特性。
让咱们看如何建立初始化 worker 吧:
var worker = new Worker('task.js');
若是 『task.js』文件存在且可访问,浏览器会生成一个线程来异步下载文件。当下载完成的时候,文件会当即执行而后 worker 开始运行。万一文件不存在,worker 会运行失败且没有任何提示。
为了启动建立的 worker,你须要调用 postMessage
方法:
worker.postMessage();
为了在 Web Worker 和 建立它的页面间进行通讯,你得使用 postMessage
方法或者一个广播信道。
最新的浏览器支持方法的第一参数为一个 JSON
对象而旧的浏览器只支持字符串。
让咱们来看一个例子,经过往 worker 的方法的第一个参数传入更为复杂的 JSON
对象来理解其建立者页面是如何与之进行来回通讯的。传入字符串与之相似。
让咱们看下如下的 HTML 页面(或者更准确地说是 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);
当点击按钮,会在主页面调用 postMessage
方法。
worker.postMessage
行代码会把包含 cmd
和 data
属性及其各自属性值的 JSON
对象传入 worker。worker 经过定义监听 message
事件来处理传过来的消息。
当接收到消息的时候,worker 会执行实际的计算而不会阻塞事件循环。worker 会检查传进来的 e
事件,而后像一个标准的 JavaScript 函数那样运行。当运行结束,传回主页面计算结果。
在 worker 的上下文中,self
和 this
都指向 worker 的全局做用域。
有两种方法来中断 woker 的执行:主页面中调用
worker.terminate()
或者在 workder 内部调用self.close()
Broadcast Channel 是更为广泛的通讯接口。它容许咱们向共享同一个源的全部上下文发送消息。同一个源下的全部的浏览器选项卡,内联框架或者 workers 均可以发送和接收消息:
// 链接到一个广播信道 var bc = new BroadcastChannel('test_channel'); // 发送简单信息示例 bc.postMessage('This is a test message.'); // 一个在控制台打印消息的简单事件处理程序示例 // logs the message to the console bc.onmessage = function (e) { console.log(e.data); } // 关闭信道 bc.close()
视觉上看,你能够经过广播信道的图例以更加深入的理解它。
全部的浏览器上下文都是同源的
然而,广播信道浏览器兼容性不太好:
有两种向 Web Workers 发送消息的方法:
因为 Web Workers 的多线程特性,它只能使用一部分 JavaScript 功能。如下是可以使用的功能列表:
navigator
对象location
对象(只读)XMLHttpRequest
setTimeout()/clearTimeout()
和 setInterval()/clearInterval()
importScripts
来引用外部脚本使人沮丧的是,Web Workers 不可以访问一些很是关键的 JavaScript 功能:
window
对象document
对象parent
对象这意味着 Web Worker 不可以操做 DOM(所以不能更新 UI)。有时候,这会让人很蛋疼,不过一旦你学会如何合理地使 Web Workers,你就会把它当成单独的『计算机器』来使用而用其它页面代码来操做 UI。Workers 将会为你完成繁重的计算任务而后一旦任务完成,会把结果传到页面中并对界面进行必要的更新。
和任何 JavaScript 代码同样,你会想要处理 Web Workers 中的任何错误。当在 worker 执行过程当中有错误发生的时候,会触发 ErrorEvent
事件。这个接口包含三个有用的属性来指出错误的地方:
示例:
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
事件。
在 worker 中(在 workerWithError
中),咱们经过未在做用域中定义的 x
乘以 2 来建立一个意图错误。异常会传播到初始化脚本(即主页面中)而后调用 onError 并传入关于错误的信息。
迄今为止,咱们列举了 Web Workers 的长处及其限制。让咱们看看他们的最佳使用场景:
射线追踪:射线追踪是一项经过追踪光线的路径做为像素来生成图片的渲染技术。Ray tracing 使用 CPU 密集型计算来模仿光线的路径。思路即模仿一些诸如反射,折射,材料等的效果。全部的这些计算逻辑能够放在 Web Worker 中以免阻塞 UI 线程。甚至更好的方法即-你能够轻易地把把图片的渲染拆分在几个 workers 中进行(即在各自的 CPU 中进行计算,意思是说利用多个 CPU 来进行计算,能够参考下 nodejs 的 api)。这里有一个使用 Web Workers 来进行射线追踪的简单示例-https://nerget.com/rayjs-mt/rayjs.html。
加密:端到端的加密因为对保护我的和敏感数据日益严格的法律规定而变得愈来愈流行。加密有时候会很是地耗时,特别是若是当你须要常常加密不少数据的时候(好比,发往服务器前加密数据)。这是一个使用 Web Worker 的绝佳场景,由于它并不须要访问 DOM 或者利用其它魔法-它只是纯粹使用算法进行计算而已。一旦在 worker 进行计算,它对于用户来讲是无缝地且不会影响到用户体验。
预取数据:为了优化网站或者网络应用及提高数据加载时间,你可使用 Workers 来提早加载部分数据以备不时之需。不像其它技术,Web Workers 在这种状况下是最棒哒,由于它不会影响程序的使用体验。
渐进式网络应用:即便在网络不稳定的状况下,它们必须快速加载。这意味着数据必须本地存储于浏览器中。这时候 IndexDB 及其它相似的 API 就派上用场了。大致上说,一个客户端存储是必须的。为了避免阻塞 UI 线程的渲染,这项工做必须由 Web Workers 来执行。呃,当使用 IndexDB的时候,能够不使用 workers 而使用其异步接口,可是以前它也含有同步接口(可能会再次引入 ),这时候就必须在 workers 中使用 IndexDB。
这里须要注意的是在现代浏览器已经不支持同步接口了,具体可查看这里。
拼写检查:一个基本的拼写检测器是这样工做的-程序会读取一个包含拼写正确的单词列表的字典文件。字典会被解析成一个搜索树以加快实际的文本搜索。当检查器检查一个单词的时候,程序会在预构建搜索树中进行检索。若是在树中没有检索到,则会经过提供替代的字符为用户提供替代的拼写并检测单词是不是有效-是不是用户须要的单词。这个检索过程当中的全部工做均可以交由 Web Worker 来完成,这样用户就只需输入单词和语句而不会阻塞 UI,与此同时 worker 会处理全部的搜索和服务建议。
实际工做过程会遇到用户须要经过解析远程图片来得到图片 base64 的案例,那么这时候,若是图片很是大,就会形成 canvas 的 toDataURL
操做至关的耗时,从而阻塞页面的渲染。
因此解决思路即把这里的处理图片的操做交由 worker 来处理。如下贴出主要的代码:
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <title>Canvas to base64</title> </head> <body> <script> function loadImageAsync(url) { if (typeof url !== 'string') { return Promise.reject(new TypeError('must specify a string')); } return new Promise(function(resolve, reject) { const image = new Image(); // 容许 canvas 跨域加载图片 image.crossOrigin="anonymous"; image.onload = function() { const $canvas = document.createElement('canvas'); const ctx = $canvas.getContext('2d'); const width = this.width; const height = this.height; let imageData; $canvas.width = width; $canvas.height = height; ctx.drawImage(image, 0, 0, width, height); imageData = ctx.getImageData(0, 0, $canvas.width, $canvas.height); resolve({image, imageData}); }; image.onerror = function() { reject(new Error('Could not load image at ' + url)); }; image.src = url; }); } function blobToDataURL(blob) { return new Promise((fulfill, reject) => { let reader = new FileReader(); reader.onerror = reject; reader.onload = (e) => fulfill(reader.result); reader.readAsDataURL(blob); }) } document.addEventListener("DOMContentLoaded", function () { loadImageAsync('https://cdn-images-1.medium.com/max/1600/1*4lHHyfEhVB0LnQ3HlhSs8g.png') .then(function (image) { // jpeg-web-worker.js https://github.com/kentmw/jpeg-web-worker const worker = new Worker('jpeg-web-worker.js'); worker.postMessage({ image: image.imageData, quality: 50 }); worker.onmessage = function(e) { // e.data is the imageData of the jpeg. {data: U8IntArray, height: int, width: int} // you can still convert the jpeg imageData into a blog like this: const blob = new Blob( [e.data.data], {type: 'image/png'} ); blobToDataURL(blob).then((imageURL) => { console.log('imageUrl:', imageURL); }) } }) .catch(function (err) { console.log('Error:', err.message); }); }); </script> </body> </html>
以上是经过 canvas 来获取图片数据,那么是否有其它方法呢?确定有的啦,动下脑筋吧少年。