Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,他们可以使用XMLHttpRequest执行 I/O (尽管responseXML和通道属性总是为空)。一旦创建, 一个worker 可以将消息发送到创建它的JavaScript代码, 通过将消息发布到该代码指定的事件处理程序 (反之亦然)。
如我们所知,JavaScript 一直是属于单线程环境,我们无法同时运行两个 JavaScript 脚本;
但是试想一下,如果我们可以同时运行两个(或者多个)JavaScript 脚本,一个来处理 UI 界面(一直以来的用法),一个来处理一些复杂计算,那么程序的整个架构将会发生很多变化,我们的任务将更有区分性和条理性,同时可以更充分利用设备的硬件计算能力(多核运算),这将大大有利于提高我们的页面性能。
在 HTML5 的新规范中,实现了 Web Worker 来引入 JavaScript 的 “多线程” 技术,他的能力让我们可以在页面主运行的 JavaScript 线程中加载运行另外单独的一个或者多个 JavaScript 线程;
当然 Web Worker 提供的多线程编程能力并不像我们传统意义上的多线程编程,它不像其他的多线程语言(Java、C++ 等),主程序线程和 Worker 线程之间,Worker 线程之间,不会共享任何作用域或资源,他们间唯一的通信方式就是一个基于事件监听机制的 message(下文将具体描述);
同时,这并不意味着 JavaScript 语言本身就支持了多线程,对于 JavaScript 语言本身它仍是运行在单线程上的, Web Worker 只是浏览器(宿主环境)提供的一个能力/API。
(以下例子统一约定:main.js 为页面运行的主要脚本文件,workder.js 为 Web Worker 脚本的文件)
实例化运行一个 Worker 很简单,我们只需要 new
一个 Worker
全局对象即可: new Worker(filepathname)
, 接受一个 filepathname String 参数,用于指定 Worker 脚本文件的路径;
// main.js var worker = new Worker('./worker.js');
// worker.js console.log('WORKER TASK: ', 'running');
用浏览器打开,我们可以看到 console 答应出来:
WORKER TASK: running worker.js:1
说明我们加载并且执行到了这个 Worker 脚本。
当实例运行了一个 Worker 线程之后,两个线程是运行在完全独立的环境中,他们之间的通信是通过基于事件监听机制的 message 来实现的, new Worker()
之后会返回一个实例对象,它包含一个 postMessage
方法,可以通过调用这个方法来给 Worker 线程传递信息;同时我们可以给这个对象监听事件,这样,就能在 Worker 中触发事件通信的时候接收到数据了;具体实现:
// main.js var worker = new Worker('./worker.js'); // 监听事件 worker.addEventListener('message', function (e) { console.log('MAIN: ', 'RECEIVE', e.data); }); // 或者可以使用 onMessage 来监听事件: // worker.onmessage = function () { // console.log('MAIN: ', 'RECEIVE', e.data); //}; // 触发事件,传递信息给 Worker worker.postMessage('Hello Worker, I am main.js');
在 Worker 的脚本中,我们可以调用全局函数 postMessage
和给全局的 onmessage
赋值来发送和监听数据和事件:
// worker.js console.log('WORKER TASK: ', 'running'); // 监听事件 onmessage = function (e) { console.log('WORKER TASK: ', 'RECEIVE', e.data); // 发送数据事件 postMessage('Hello, I am Worker'); } // 或者使用 addEventListener 来监听事件 //addEventListener('message', function (e) { // console.log('WORKER TASK: ', 'RECEIVE', e.data); // ... //});
可以在 console 中看到,我们完成了一次两个线程之间的数据通信。
当然,这里传递的是一个 String 类型的数据,实际上,它支持 JavaScript 中所有类型的数据传递,可以传递一个 Object 数据;然而,值得注意的是,这里的数据传递(主要是 Object 类型)并不是共享,而是复制,发送端的数据和接收端的数据是复制而来,并不指向同一个对象,并且,这里的复制不是简单的便利拷贝,而是通过两端的序列化/解序列化来实现的,一般来说浏览器会通过 JSON 编码/解码;当然,这里的更多细节部分会由浏览器来处理,我们并不需要关系,只需要明白两端的数据是复制而来,互相独立的。
如果在某个时机不想要 Worker 继续运行了,那么我们需要终止掉这个线程,可以调用 worker
的 terminate
方法 :
var worker = new Worker('./worker.js'); ... worker.terminate();
当我们需要监听 worker 出现运行时错误的时候,可以在 worker
对象监听 error
事件:
// main.js var worker = new Worker('./worker.js'); // 监听消息事件 worker.addEventListener('message', function (e) { console.log('MAIN: ', 'RECEIVE', e.data); }); // 或者可以使用 onMessage 来监听事件: // worker.onmessage = function () { // console.log('MAIN: ', 'RECEIVE', e.data); //}; // 监听 error 事件 worker.addEventListener('error', function (e) { console.log('MAIN: ', 'ERROR', e); console.log('MAIN: ', 'ERROR', 'filename:' + e.filename + '---message:' + e.message + '---lineno:' + e.lineno); }); // 或者可以使用 onMessage 来监听事件: // worker.onerror = function () { // console.log('MAIN: ', 'ERROR', e); //}; // 触发事件,传递信息给 Worker worker.postMessage({ m: 'Hello Worker, I am main.js' });
// worker.js console.log('WORKER TASK: ', 'running'); // 监听事件 onmessage = function (e) { console.log('WORKER TASK: ', 'RECEIVE', e.data); // 发送数据事件 // 注意:这里的 hhh 变量在 worker.js 中并未定义,所以这里执行过程中会错处 postMessage( hhh ); }
运行程序可以才 console 看到,main.js 中接收到来自 worker.js 中的一个运行错误,在监听事件的函数中接受一个参数 event
这个事件对象中有几个比较重要的参数需要我们注意:
如前文所述,在 Worker 线程的运行环境中没有 window 全局对象,也无法访问 DOM 对象,所以一般来说他只能来执行纯 JavaScript 的计算操作。
但是,他还是可以获取到部分浏览器提供的 API 的:
setTimeout(), clearTimeout(), setInterval(), clearInterval()
:有了设计个函数,就可以在 Worker 线程中执行定时操作了; XMLHttpRequest
对象:意味着我们可以在 Worker 线程中执行 ajax 请求; navigator
对象:可以获取到 ppName,appVersion,platform,userAgent 等信息; location
对象(只读):可以获取到有关当前 URL 的信息; 可以通过 Worker 环境中的全局函数 importScripts()
加载外部 js 脚本到当前 Worker 脚本中,它接收多个参数,参数都为加载脚本的链接字符串,比如:
// main.js var worker = new Worker('./worker1.js');
// worker1.js console.log('hello, I,m worker 1'); importScripts('worker2.js', 'worker3.js'); // 或者 // importScripts('worker2.js'); // importScripts('worker3.js');
// worker2.js console.log('hello, I,m worker 2');
// worker3.js console.log('hello, I,m worker 3');
在这里,我们在 main.js 中运行了 worker1.js 线程,然后在 worker1.js 中加载了 worker2.js 和 worker3.js,在 console 中,可以看到他们全部执行了。
我们可以在一个 Worker 脚本中去实例化另一个 Worker,这成为子 Worker,但是这个特性目前大部分浏览器还未实现,所以不展开阐述。
对于 Web Worker ,一个 tab 页面只能对应一个 Worker 线程,是相互独立的;
而 SharedWorker 提供了能力能够让不同标签中页面共享的同一个 Worker 脚本线程;
当然,有个很重要的限制就是它们需要满足同源策略,也就是需要在同域下;
在页面(可以多个)中实例化 Worker 线程:
// main.js var myWorker = new SharedWorker("worker.js"); myWorker.port.start(); myWorker.port.postMessage("hello, I'm main"); myWorker.port.onmessage = function(e) { console.log('Message received from worker'); }
// worker.js onconnect = function(e) { var port = e.ports[0]; port.addEventListener('message', function(e) { var workerResult = 'Result: ' + (e.data[0] * e.data[1]); port.postMessage(workerResult); }); port.start(); }
在 SharedWorker 的使用中,我们发现对于 SharedWorker 实例对象,我们需要通过 port 属性来访问到主要方法;
同时在 Worker 脚本中,多了个全局的 connect()
函数,同时在函数中也需要去获取一个 post 对象来进行启动以及操作;
这是因为,多页页面共享一个 SharedWorker 线程时,在线程中需要去判断和区分来自不同页面的信息,这是最主要的区别和原因。
遗憾的是,对于 SharedWorker,兼容性现在而言也是大部分浏览器还未实现。
Web Worker
http://caniuse.com/#feat=webworkers
Web Worker 的实现为前端程序带来了后台计算的能力,可以实现主 UI 线程与复杂计运算线程的分离,从而极大减轻了因计算量大而造成 UI 阻塞而出现的界面渲染卡、掉帧的情况,并且更大程度地利用了终端硬件的性能;
同时把程序之间的任务更清晰、条理化;
其主要应用有几个场景: