JavaScript 中的多线程 -- Web Worker

Web Worker 介绍

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。


上手使用

实例化一个 Worker

(以下例子统一约定:main.js 为页面运行的主要脚本文件,workder.js 为 Web Worker 脚本的文件)

实例化运行一个 Worker 很简单,我们只需要 `new` 一个 `Worker` 全局对象即可:`new Worker(filepathname)`, 接受一个 filepathname String 参数,用于指定 Worker 脚本文件的路径;

// main.jsvar worker = new Worker('./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.jsconsole.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 继续运行了,那么我们需要终止掉这个线程,可以调用 `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.jsconsole.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 的环境与作用域

如前文所述,在 Worker 线程的运行环境中没有 window 全局对象,也无法访问 DOM 对象,所以一般来说他只能来执行纯 JavaScript 的计算操作。

但是,他还是可以获取到部分浏览器提供的 API 的:


在 Worker 中加载外部脚本

可以通过 Worker 环境中的全局函数 `importScripts()` 加载外部 js 脚本到当前 Worker 脚本中,它接收多个参数,参数都为加载脚本的链接字符串,比如:

// main.jsvar worker = new Worker('./worker1.js');
// worker1.jsconsole.log('hello, I,m worker 1');importScripts('worker2.js', 'worker3.js');// 或者// importScripts('worker2.js');// importScripts('worker3.js');
// worker2.jsconsole.log('hello, I,m worker 2');
// worker3.jsconsole.log('hello, I,m worker 3');

在这里,我们在 main.js 中运行了 worker1.js 线程,然后在 worker1.js 中加载了 worker2.js 和 worker3.js,在 console 中,可以看到他们全部执行了。


subworker -- Worker 中的 Worker


我们可以在一个 Worker 脚本中去实例化另一个 Worker,这成为子 Worker,但是这个特性目前大部分浏览器还未实现,所以不展开阐述。



SharedWorker

对于 Web Worker ,一个 tab 页面只能对应一个 Worker 线程,是相互独立的;


而 SharedWorker 提供了能力能够让不同标签中页面共享的同一个 Worker 脚本线程;


当然,有个很重要的限制就是它们需要满足同源策略,也就是需要在同域下;


在页面(可以多个)中实例化 Worker 线程:


// main.jsvar 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.jsonconnect = 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 的兼容性

Web Worker


Can I use... Support tables for HTML5, CSS3, etc



应用

Web Worker 的实现为前端程序带来了后台计算的能力,可以实现主 UI 线程与复杂计运算线程的分离,从而极大减轻了因计算量大而造成 UI 阻塞而出现的界面渲染卡、掉帧的情况,并且更大程度地利用了终端硬件的性能;

同时把程序之间的任务更清晰、条理化;

其主要应用有几个场景:


reference