在浏览器中,因为 JavaScript 引擎与 GUI 渲染线程是互斥的,因此当咱们在 JavaScript 中执行一些计算密集型或高延迟的任务的时候,会致使页面渲染被阻塞或拖慢。为了解决这个问题,提升用户体验,HTML5 为咱们带来了 Web Workers 这一标准。javascript
做为 HTML5 标准中的一部分,Web Workers
定义了一套 API,容许一段 JavaScript 程序运行在主线程以外的 Worker 线程中。 在主线程运行的同时,Worker 线程能够在后台独立运行,处理一些计算密集型或高延迟的任务,等 Worker 线程完成计算任务,再把结果返回给主线程。从而保证主线程(一般是 UI 线程)不会所以被阻塞或拖慢。html
常见的 Web Workers 主要有如下三种类型:java
即专用 Workers
, 仅能被生成它的脚本所使用,且只能与一个页面渲染进程进行绑定和通讯, 不能多 Tab 共享。浏览器的支持状况以下图:编程
咱们在主线程 JS 中调用 new 命令,而后实列化 Worker()构造函数,就能够建立一个 Worker 线程了,代码以下所示:浏览器
var worker = new Worker("work.js");
Worker()
构造函数的参数是一个脚本文件,该文件就是 Worker 线程须要执行的任务,须要注意的是,因为 Web Workers 有同源限制,所以这个脚本必须从网络或者本地服务器读取。缓存
接下来,咱们就能够从主线程向子线程发送消息了,使用 worker.postMessage()
方法,向 Worker 发送消息。代码以下所示:服务器
worker.postMessage("Hello LeapFE");
worker.postMessage
方法能够接受任何类型的参数,甚至包括二进制数据。网络
Worker 线程内部须要有一个监听函数,监听主线程/其余子线程 发送过来的消息。监听事件为 message
. 代码以下所示:dom
addEventListener('message', function(e) { postMessage('子线程向主线程发送消息: ' + e.data); close(); // 关闭自身 });`
子线程接收到主进程发来的数据,而后执行相应的操做,最后把结果再返回给主线程,异步
主线程经过 worker.onmessage
指定监听函数,接收子线程传送回来的消息,代码以下所示:
worker.onmessage = function (event) { console.log("接收到的消息为: " + event.data); };
从事件对象的 data 属性中能够获取到 Worker 发送回来的消息。
若是咱们的 Worker 线程任务完成后,咱们的主线程须要把它关闭掉,代码以下所示:
worker.terminate();
Worker 内部若是须要加载其余的脚本的话,咱们可使用 importScripts()
方法。代码以下所示:
importScripts("a.js");
若是要加载多个脚本的话,代码能够写成这样:
importScripts('a.js', 'b.js', 'c.js', ....);
主线程能够监听 Worker 线程是否发生错误,若是发生错误,Worker 线程会触发主线程的 error
事件。
worker.onerror = function (e) { console.log(e); };
若是是在 Worker 中若是发生错误的话, 能够经过throw new Error()
将错误暴露出来,但这个错误没法被主线程获取,只能在 Worker 的 console
中看到“错误未捕获提示”的错误提示,而不是主线程的 console
!
// worker.js内部: // ... other code throw new Error("test error");
即共享 Workers
, 能够看做是专用 Workers 的拓展,除了支持专用 Workers 的功能以外,还能够被不一样的 window 页面,iframe,以及 Worker 访问(固然要遵循同源限制),从而进行异步通讯。浏览器的支持状况以下图:
建立共享 Workers 能够经过使用 SharedWorker() 构造函数来实现,这个构造函数使用 URL 做为第一个参数,便是指向 JavaScript 资源文件的 URL。代码以下所示:
var worker = new SharedWorker("sharedworker.js");
共享 Workers 与主线程交互的步骤和专用 Worker 基本同样,只是多了一个 port:
// 主线程: const worker = new SharedWorker("worker.js"); const key = Math.random().toString(32).slice(-6); worker.port.postMessage(key); worker.port.onmessage = (e) => { console.log(e.data); };
// worker.js: const buffer = []; onconnect = function (evt) { const port = evt.ports[0]; port.onmessage = (m) => { buffer.push(m.data); port.postMessage("worker receive:" + m.data); }; };
在上面的代码中,须要注意的地方有两点:
onconnect
当其余线程建立 sharedWorker 实际上是向 sharedWorker 发了一个连接,worker 会收到一个 connect 事件evt.ports[0]
connect 事件的句柄中 evt.ports[0]是很是重要的对象 port,用来向对应线程发送消息和接收对应线程的消息在目前阶段,Service Worker 的主要能力集中在网络代理和离线缓存上。具体的实现上,能够理解为 Service Worker 是一个能在网页关闭时仍然运行的 Web Worker。浏览器的支持状况以下图:
PS: Service Workers
涉及的功能点比较多,因篇幅有限,本文将暂不进行介绍,咱们会在后面的更新中再详细解析。
如上文已经提到的,Worker 能够在后台独立运行,不阻塞主进程,最多见的使用 Worker 的场景就是处理一些计算密集型或高延迟的任务。
咱们在页面中有一个 input 输入框,用户须要在该输入框中输入数字,而后点击旁边的计算按钮,在后台计算从 1 到给定数值的总和。若是咱们不使用 Web Workers 来解决该问题的话,以下 demo 代码所示:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Web Worker</title> </head> <body> <h1>从1到给定数值的求和</h1> 输入数值: <input type="text" id="num" /> <button onclick="calculate()">计算</button> <script type="text/javascript"> function calculate() { var num = parseInt(document.getElementById("num").value, 10); var result = 0; // 循环计算求和 for (var i = 0; i <= num; i++) { result += i; } alert("总和为:" + result + "。"); } </script> </body> </html>
如上代码,而后咱们输入 1 百亿,而后让计算机去帮咱们计算,计算的时间应该要 20 秒左右的时间,可是在这 20 秒以前的时间,那么咱们的页面就处于卡顿的状态,也就是说什么都不能作,等计算结果出来后,咱们就会看到以下弹窗提示结果了,以下所示:
那如今咱们尝试使用 Web Workers 来解决该问题,把这些耗时操做使用 Worker 去解决,那么主线程就不影响页面假死的状态了,咱们首先把 index.html 代码改为以下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Web Worker</title> </head> <body> <h1>从1到给定数值的求和</h1> 输入数值: <input type="text" id="num" /> <button id="calculate">计算</button> <script type="module"> // 建立 worker 实列 var worker = new Worker("./worker1.js"); var calDOM = document.getElementById("calculate"); calDOM.addEventListener("click", calculate); function calculate() { var num = parseInt(document.getElementById("num").value, 10); // 将咱们的数据传递给 worker 线程,让咱们的 worker 线程去帮咱们作这件事 worker.postMessage(num); } // 监听 worker 线程的结果 worker.onmessage = function (e) { alert("总和值为:" + e.data); }; </script> </body> </html>
如上代码咱们运行下能够看到,咱们点击下计算按钮后,咱们使用主线程把该复杂的耗时操做给子线程处理后,咱们点击按钮后,咱们的页面就能够操做了,由于主线程和 Worker 线程是两个不一样的环境,Worker 线程的不会影响主线程的。所以若是咱们须要处理一些耗时操做的话,咱们可使用 Web Workers 线程去处理该问题。
下面咱们给出一个例子:建立一个共享 Worker 共享多个 Tab 页的数据,实现一个简单的网页聊天室的功能。
首先在 index.html
中设计简单的聊天对话框样式, 同时引入 main.js:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Shared Worker Example</title> <style> ul li { float: left; list-style: none; margin-top: 10px; width: 100%; } ul li > span { font-size: 10px; transform: scale(0.8); display: block; width: 17%; } ul li > p { background: rgb(140, 222, 247); border-radius: 4px; padding: 4px; margin: 0; display: inline-block; } ul li.right { float: right; text-align: right; } ul li.right > p { background: rgb(132, 226, 140); } ul li.right > span { width: 110%; } #chatList { width: 300px; background: #fff; height: 400px; padding: 10px; border: 4px solid #de8888; border-radius: 10px; } </style> </head> <body> <div class="container"> <section> <p id="user"></p> <ul id="chatList" style="width: 300px"></ul> <input id="input" /> <button id="submitBtn">提交</button> </section> </div> <script src="./main.js"></script> </body> </html>
在 main.js 中,咱们初始化一个 SharedWorker
实例
window.onload = () => { const worker = new SharedWorker("./shared-worker.js"); const chatList = document.querySelector("#chatList"); let id = null; worker.port.onmessage = (event) => { const { data } = event; switch (data.action) { case "id": // 接收 Worker 实例化成功以后返回的 id id = data.value; document.querySelector("#user").innerHTML = `Client ${id}`; break; case "message": // 接收 Worker 返回的来自各个页面的信息 chatList.innerHTML += `<li class="${ data.id === id ? "right" : "left" }"><span>Client ${data.id}</span><p>${data.value}</p></li>`; break; default: break; } }; document.querySelector("#submitBtn").addEventListener("click", () => { const value = document.querySelector("#input").value; // 将当前用户 ID 及消息发送给 Worker worker.port.postMessage({ action: "message", value: value, id, }); }); };
在 shared-worker.js
接收与各页面的链接,同时转发页面发送过来的消息
const connectedClients = new Set(); let connectID = 1; function sendMessageToClients(payload) { //将消息分发给各个页面 connectedClients.forEach(({ client }) => { client.postMessage(payload); }); } function setupClient(clientPort) { //经过 onmessage 监听来自主进程的消息 clientPort.onmessage = (event) => { const { id, value } = event.data; sendMessageToClients({ action: "message", value: value, id: connectID, }); }; } // 经过 onconnect 函数监听,来自不一样页面的 Worker 链接 onconnect = (event) => { const newClient = event.ports[0]; // 保存链接到 Worker 的页面引用 connectedClients.add({ client: newClient, id: connectID, }); setupClient(newClient); // 页面同 Worker 链接成功后, 将当前链接的 ID 返回给页面 newClient.postMessage({ action: "id", value: connectID, }); connectID++; };
在上面的共享线程例子中,在主页面即各个用户链接页面构造出一个共享线程对象,而后经过 worker.port.postMessage
向共享线程发送用户输入的信息。同时,在共享线程的实现代码片断中定义 connectID
, 用来记录链接到这个共享线程的总数。以后,用 onconnect
事件处理器接收来自不一样用户的链接,解析它们传递过来的信息。最后,定义一个了方法 sendMessageToClients
将消息分发给各个用户。
Web Workers 确实给咱们提供了优化 Web 应用的新可能,经过使用 Web Workers 来合理地调度 JavaScript 运行逻辑,能够在面对没法预测的低端设备和长任务时,保证 GUI 依旧是可响应的。
或许将来,使用 Web Workers 进行编程或许会成为新一代 Web 应用开发的标配或是最佳实践。