本文由云+社区发表html
JavaScript 语言采用的是单线程模型,也就是说,全部任务只能在一个线程上完成,一次只能作一件事。前面的任务没作完,后面的任务只能等着。随着电脑计算能力的加强,尤为是多核 CPU 的出现,单线程带来很大的不便,没法充分发挥计算机的计算能力。web
Web Worker 的做用,就是为 JavaScript 创造多线程环境,容许主线程建立 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,二者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(一般负责 UI 交互)就会很流畅,不会被阻塞或拖慢。json
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(好比用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通讯。可是,这也形成了 Worker 比较耗费资源,不该该过分使用,并且一旦使用完毕,就应该关闭。api
Web Worker 有如下几个使用注意点。浏览器
(1)同源限制缓存
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。服务器
(2)DOM 限制网络
Worker 线程所在的全局对象,与主线程不同,没法读取主线程所在网页的 DOM 对象,也没法使用document
、window
、parent
这些对象。可是,Worker 线程能够navigator
对象和location
对象。多线程
(3)通讯联系app
Worker 线程和主线程不在同一个上下文环境,它们不能直接通讯,必须经过消息完成。
(4)脚本限制
Worker 线程不能执行alert()
方法和confirm()
方法,但可使用 XMLHttpRequest 对象发出 AJAX 请求。
(5)文件限制
Worker 线程没法读取本地文件,即不能打开本机的文件系统(file://
),它所加载的脚本,必须来自网络。
主线程采用new
命令,调用Worker()
构造函数,新建一个 Worker 线程。
var worker = new Worker('work.js');
Worker()
构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。因为 Worker 不能读取本地文件,因此这个脚本必须来自网络。若是下载没有成功(好比404错误),Worker 就会默默地失败。
而后,主线程调用worker.postMessage()
方法,向 Worker 发消息。
worker.postMessage('Hello World'); worker.postMessage({method: 'echo', args: ['Work']});
worker.postMessage()
方法的参数,就是主线程传给 Worker 的数据。它能够是各类数据类型,包括二进制数据。
接着,主线程经过worker.onmessage
指定监听函数,接收子线程发回来的消息。
worker.onmessage = function (event) { console.log('Received message ' + event.data); doSomething(); } function doSomething() { // 执行任务 worker.postMessage('Work done!'); }
上面代码中,事件对象的data
属性能够获取 Worker 发来的数据。
Worker 完成任务之后,主线程就能够把它关掉。
worker.terminate();
Worker 线程内部须要有一个监听函数,监听message
事件。
self.addEventListener('message', function (e) { self.postMessage('You said: ' + e.data); }, false);
上面代码中,self
表明子线程自身,即子线程的全局对象。所以,等同于下面两种写法。
// 写法一 this.addEventListener('message', function (e) { this.postMessage('You said: ' + e.data); }, false); // 写法二 addEventListener('message', function (e) { postMessage('You said: ' + e.data); }, false);
除了使用self.addEventListener()
指定监听函数,也可使用self.onmessage
指定。监听函数的参数是一个事件对象,它的data
属性包含主线程发来的数据。self.postMessage()
方法用来向主线程发送消息。
根据主线程发来的数据,Worker 线程能够调用不一样的方法,下面是一个例子。
self.addEventListener('message', function (e) { var data = e.data; switch (data.cmd) { case 'start': self.postMessage('WORKER STARTED: ' + data.msg); break; case 'stop': self.postMessage('WORKER STOPPED: ' + data.msg); self.close(); // Terminates the worker. break; default: self.postMessage('Unknown command: ' + data.msg); }; }, false);
上面代码中,self.close()
用于在 Worker 内部关闭自身。
Worker 内部若是要加载其余脚本,有一个专门的方法importScripts()
。
importScripts('script1.js');
该方法能够同时加载多个脚本。
importScripts('script1.js', 'script2.js');
主线程能够监听 Worker 是否发生错误。若是发生错误,Worker 会触发主线程的error
事件。
worker.onerror(function (event) { console.log([ 'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message ].join('')); }); // 或者 worker.addEventListener('error', function (event) { // ... });
Worker 内部也能够监听error
事件。
使用完毕,为了节省系统资源,必须关闭 Worker。
// 主线程 worker.terminate(); // Worker 线程 self.close();
前面说过,主线程与 Worker 之间的通讯内容,能够是文本,也能够是对象。须要注意的是,这种通讯是拷贝关系,便是传值而不是传址,Worker 对通讯内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通讯内容串行化,而后把串行化后的字符串发给 Worker,后者再将它还原。
主线程与 Worker 之间也能够交换二进制数据,好比 File、Blob、ArrayBuffer 等类型,也能够在线程之间发送。下面是一个例子。
// 主线程 var uInt8Array = new Uint8Array(new ArrayBuffer(10)); for (var i = 0; i < uInt8Array.length; ++i) { uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...] } worker.postMessage(uInt8Array); // Worker 线程 self.onmessage = function (e) { var uInt8Array = e.data; postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString()); postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength); };
可是,拷贝方式发送二进制数据,会形成性能问题。好比,主线程向 Worker 发送一个 500MB 文件,默认状况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 容许主线程把二进制数据直接转移给子线程,可是一旦转移,主线程就没法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫作Transferable Objects。这使得主线程能够快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就很是方便了,不会产生性能负担。
若是要直接转移数据的控制权,就要使用下面的写法。
// Transferable Objects 格式 worker.postMessage(arrayBuffer, [arrayBuffer]); // 例子 var ab = new ArrayBuffer(1); worker.postMessage(ab, [ab]);
一般状况下,Worker 载入的是一个单独的 JavaScript 脚本文件,可是也能够载入与主线程在同一个网页的代码。
<!DOCTYPE html>
上面是一段嵌入网页的脚本,注意必须指定<script>
标签的type
属性是一个浏览器不认识的值,上例是app/worker
。
而后,读取这一段嵌入页面的脚本,用 Worker 来处理。
var blob = new Blob([document.querySelector('#worker').textContent]); var url = window.URL.createObjectURL(blob); var worker = new Worker(url); worker.onmessage = function (e) { // e.data === 'some message' };
上面代码中,先将嵌入网页的脚本代码,转成一个二进制对象,而后为这个二进制对象生成 URL,再让 Worker 加载这个 URL。这样就作到了,主线程和 Worker 的代码都在同一个网页上面。
有时,浏览器须要轮询服务器状态,以便第一时间得知状态改变。这个工做能够放在 Worker 里面。
function createWorker(f) { var blob = new Blob([f.toString()]); var url = window.URL.createObjectURL(blob); var worker = new Worker(url); return worker; } var pollingWorker = createWorker(function (e) { var cache; function compare(new, old) { ... }; setInterval(function () { fetch('/my-api-endpoint').then(function (res) { var data = res.json(); if (!compare(data, cache)) { cache = data; self.postMessage(data); } }) }, 1000) }); pollingWorker.onmessage = function () { // render data } pollingWorker.postMessage('init');
上面代码中,Worker 每秒钟轮询一次数据,而后跟缓存作比较。若是不一致,就说明服务端有了新的变化,所以就要通知主线程。
Worker 线程内部还能再新建 Worker 线程。下面的例子是将一个计算密集的任务,分配到10个 Worker。
主线程代码以下。
var worker = new Worker('worker.js'); worker.onmessage = function (event) { document.getElementById('result').textContent = event.data; };
Worker 线程代码以下。
// worker.js // settings var num_workers = 10; var items_per_worker = 1000000; // start the workers var result = 0; var pending_workers = num_workers; for (var i = 0; i < num_workers; i += 1) { var worker = new Worker('core.js'); worker.postMessage(i * items_per_worker); worker.postMessage((i + 1) * items_per_worker); worker.onmessage = storeResult; } // handle the results function storeResult(event) { result += event.data; pending_workers -= 1; if (pending_workers <= 0) postMessage(result); // finished! }
上面代码中,Worker 线程内部新建了10个 Worker 线程,而且依次向这10个 Worker 发送消息,告知了计算的起点和终点。计算任务脚本的代码以下。
// core.js var start; onmessage = getStart; function getStart(event) { start = event.data; onmessage = getEnd; } var end; function getEnd(event) { end = event.data; onmessage = null; work(); } function work() { var result = 0; for (var i = start; i < end; i += 1) { // perform some complex calculation here result += 1; } postMessage(result); close(); }
浏览器原生提供Worker()
构造函数,用来供主线程生成 Worker 线程。
var myWorker = new Worker(jsUrl, options);
Worker()
构造函数,能够接受两个参数。第一个参数是脚本的网址(必须遵照同源政策),该参数是必需的,且只能加载 JS 脚本,不然会报错。第二个参数是配置对象,该对象可选。它的一个做用就是指定 Worker 的名称,用来区分多个 Worker 线程。
// 主线程 var myWorker = new Worker('worker.js', { name : 'myWorker' }); // Worker 线程 self.name // myWorker
Worker()
构造函数返回一个 Worker 线程对象,用来供主线程操做 Worker。Worker 线程对象的属性和方法以下。
Event.data
属性中。Web Worker 有本身的全局对象,不是主线程的window
,而是一个专门为 Worker 定制的全局对象。所以定义在window
上面的对象和方法不是所有均可以使用。
Worker 线程有一些本身的全局属性和方法。
message
事件的监听函数。此文已由做者受权腾讯云+社区发布
搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!