JavaScript多线程之一~~HTML5中的Web Worker

时间:2021-02-04 19:35:35

Web Worker是工作线程的意思(本文中如果没有特别说明,那么Web Worker是指的专有线程(Dedicated Worker),Web Worker、工作线程和专有线程在本文中是指代同样的东西),它是HTML5中新增加的概念。W3C的网站Web Workers和WHATWG的网站上都可以去查看HTML Living Standard — Last Updated 30 November 2015。Web Worker在浏览器上的支持情况:点击查看

Web Worker的出现对于JavaScript具有非常重大的意义,个人觉得Web Worker的出现就如同当年AJAX对于JavaScript前端开发的意义一样重大,这是完全不同的开发体验。因为以前的JavaScript都是运行在单线程上的,无论是在浏览器中还是在Node.js中(虽然这个概念在2008年就提出来了,比Node.js出现的时间还早,但是Node.js仍然是使用单线程来运行JavaScript,现在有Node.js的扩展可以让Node.js支持Web Worker)。因为多线程的出现对于单线程的js来讲是非常有诱惑力和想象力的,相当于为JavaScript打开了另一片广阔的天地,我非常看好Web Worker的未来前景。如果范围扩大到手机端,可以看到Android(4.4以上)和IOS(8.4以上)自带的浏览器已经支持工作线程了,大规模使用的场景已经成熟了,那么JavaScript也可以在手机浏览器上以多线程的方式运行,把一些耗时的运算或DOM的运算(WebWorker中不能使用DOM,此处指的是类似React中提出的虚拟DOM等概念)放在后台线程中,或许HTML5应用达到原生APP的运行速度就不是梦想了呢。

如果在浏览器端希望JavaScript默默的执行一些定时任务,通常使用setInterval或者setTimeout来模拟多任务,由于页面渲染的影响,setInterval等的回调函数并不是到时后立即执行,而是等系统计算资源空闲下来后才会执行。因此这个函数的执行时间非常的不准确;反过来,由setInterval和setTimeout函数启动的计算任务如果非常复杂,也会影响到页面渲染的流畅性,造成非常差的用户体验。而使用Web Worker的话,在另外一个线程运行js代码,就不会受到这类的影响。页面渲染和后台计算任务各行其是,皆大欢喜。

Web Worker的使用方法其实很简单,记住几个基本要点就可以:

DOM限制

Web Worker被设定为主线程之外运行JavaScript的线程,并不会直接与主线程里的浏览器和页面相关对象进行交互,也就是无法读取文档对象模型(DOM)和大部分浏览器对象模型(BOM)相关的信息,比如document等,window对象也有许多属性不能被Web Worker访问,但是window.navigator和window.location对象可以被获得。

另外子线程还有下列限制,详细内容可以参考资料6

  • “同域限制”:主线程子线程文件同域
  • “脚本限制”:无法执行alert、confirm
  • “文件限制”:无法读取本地文件系统

消息机制

以MDN上的例子为参考:

<script>
var myWorker = new Worker("my_task.js");

myWorker.addEventListener("message", function (oEvent) {
console.log("Called back by the worker!\n");
}, false);

myWorker.postMessage(""); // start the worker.
</script>

上述这段是主线程中的一部分脚本,my_task.js为子线程要执行的脚本。主线程通过监听子线程的message事件被动获取子线程的通知,通过执行postMessage主动向子线程发送消息。同样子线程内也是通过类似方式跟主线程互发消息。

my_task.js的代码如下:

postMessage("I\'m working before postMessage(\'ali\').");

onmessage = function (oEvent) {
postMessage("Hi " + oEvent.data);
};

子线程最基本要实现的方法就是onmessage,这个用于获取主线程的通知。postMessage用于向主线程主动发送通知。

全局作用域

Web Worker(工作线程)的作用域仅限于其本身,并且只在工作线程的生命周期内有效。此处指的是专有线程,共享线程(支持的浏览器较少)则是由创建它的域来决定的。每个工作线程的全局作用域都是独立的,相互之间不会干扰。

值传递

对于Web Worker的参数传递,简单总结就是:Web Worker与主线程之间的参数传递是使用深拷贝的方式。无论被传递的是字符串还是JSON,都是使用深拷贝的方式。

深拷贝和浅拷贝在其他的语言中都是非常明显的,浅拷贝多指对于引用类型的操作,深拷贝多指值类型的操作。在JavaScript这两者并不明显,但也是存在的。例如

<script>
var i={"a":"aa","b":"bb"}, j=i;
var f=JSON.parse(JSON.stringify(i));

j["a"]="xyz";
f["a"]="abcd";

console.log(i);
console.log(j);
console.log(f);
</script>

上述代码的运行结果是:

    Object { a: "xyz", b: "bb" }
Object { a: "xyz", b: "bb" }
Object { a: "abcd", b: "bb" }

这段代码中i给j赋值使用的是浅拷贝,i给f赋值使用的是深拷贝,结果就是f的修改并不会影响到i的值。同样的道理,主线程和工作线程两者互不影响,传递过去的数据都是具有独立生命周期和作用范围的。

<script>
var data={"name":"阿朱的主线程"};
var myWorker = new Worker("subworker.js");

myWorker.addEventListener("message", function (oEvent) {
console.log("工作线程的结果:"+oEvent.data["name"]);
console.log("主线程的结果:"+data["name"]);
}, false);

myWorker.postMessage(data); // start the worker.
</script>

上述是主线程的代码,在页面上将data传递给subworker.js生成的工作线程,然后工作线程修改后将json传递回来。subworker.js的代码如下:

var data={ };

onmessage = function (oEvent) {
data=oEvent.data;
data["name"]="阿朱的工作线程";
postMessage(data);
};

输出的结果是:

工作线程的结果:阿朱的工作线程 
主线程的结果:阿朱的主线程

子子孙孙

工作线程也可以创建自己的子线程,并且使用同样的消息机制在父子线程之间交换数据。目前没有测试这个子子孙孙会不会无穷匮,但是需要明白每个子线程的创建都是需要消耗资源的。

无测试不真相,代码说话:

<script>
var data={"name":"阿朱的主线程"};
var myWorker = new Worker("subworker.js");

myWorker.addEventListener("message", function (oEvent) {
console.log("工作线程的结果:"+oEvent.data["name"]);

}, false);

myWorker.postMessage(data); // start the worker.
console.log("主线程的结果:"+data["name"]);
</script>

上述是主线程的代码,使用subworker.js建立一个子线程,并且等待子线程将结果传递回来。
subworker.js的代码:

var data={  };

var sub=new Worker('subworker2.js');

sub.addEventListener("message",function(oEvent){
postMessage(oEvent.data);
});

onmessage = function (oEvent) {
data=oEvent.data;
data["name"]="阿朱的工作线程";
postMessage(data);
sub.postMessage(data);
};

subworker.js中使用subworker2.js建立了它的子线程(下文叫孙线程),并且监听了孙线程的返回结果并且直接返回给主线程。subworker.js在接受到消息的过程中就回馈给主线程一个消息,并且还给孙线程发了一个消息。
subworker2.js的代码:

var data={ };

onmessage = function (oEvent) {
data=oEvent.data;
data["name"]="阿朱的工作线程2";
postMessage(data);
};

subworker2.js中的工作最简单,就是监听父级线程的消息并且返回一个结果。程序的运行结果是主线程依次收到了子线程和孙线程(通过子线程转发)发出的消息。

主线程的结果:阿朱的主线程 
工作线程的结果:阿朱的工作线程
工作线程的结果:阿朱的工作线程2

资源释放

如果不是需要持续执行任务的工作线程,最好在其任务完成后关闭掉。有两个方法可以完成这个工作:
webworker.terminate()这个是在主线程用于关闭子线程的;
self.close()这个是子线程关闭自身的方法。

引入脚本

在Web Worker的代码中也可以引入其他的脚本。这个工作是通过全局函数importScripts()来引入的。被引入的脚本中的全局变量可以被当前的Worker直接使用。需要注意的是此处同样受到同域访问的限制,不能跨域访问。importScripts在下载完成并解析完成后才会继续执行后续代码。主线程的代码:

<script>
var myWorker = new Worker("subworker.js");

myWorker.addEventListener("message", function (oEvent) {
console.log("工作线程的结果:"+oEvent.data);
}, false);

myWorker.postMessage({"name":"1"});
</script>

subworker.js的代码:

onmessage = function (oEvent) {   
importScripts('externalScript.js');
postMessage(gloableVar);
};

externalScript.js的代码:

var gloableVar="我是外部脚本";

运行结果:工作线程的结果:我是外部脚本

资源占用

我写了一段代码,测试了启动Worker占用的资源:

<input type="button" id="btnAddWorker" value="增加线程"/>
<input type="button" id="btnShtWorker" value="关闭线程"/>
<input type="text" id="workerNum"/>
<script>
var i=0;
var workers=[];
$(document).ready(function(){
$("#btnAddWorker").click(function(){
var myWorker = new Worker("subworker.js");

myWorker.addEventListener("message", function (oEvent) {
console.log("工作线程的结果:"+oEvent.data["name"]);

}, false);
workers.push(myWorker);
$("#workerNum").val(++i);
});
$("#btnShtWorker").click(function(){

var worker=workers.shift();
worker.terminate();
$("#workerNum").val(--i);
});
});
</script>

subworker.js的代码可以参考上个章节的样例。经过测试发现,每次启动一个Web Worker大概会让任务管理器里面Firefox内存上涨5M左右,执行teminate方法之后内存也会相应下降5M左右。至于其他的浏览器会怎样,小伙伴们可以自行测试。

浏览器支持

目前在基本所有最新的浏览器(截止到2015年底)都可以支持Web Worker(还有个别浏览器支持共享线程,这个比较少所以咱不讨论)。IE10和IE11中都可以使用Web Worker,而IE9及以前版本不能使用。为了应对浏览器兼容性现状,需要在开发Web Worker相关的库时考虑到不兼容的情况,正确的做法是使用Modernizr库,例如:

if (Modernizr.webworkers) {
//支持Worker时的代码
}
else {
//不支持Worker时的代码
}

或者,干脆直接判断Worker是否存在

if (window.Worker) {
//支持Worker时的代码
}
else {
//不支持Worker时的代码
}

Worker使用实例

以Web Worker后台运行刷新数据为例。假设这样一个使用场景:工作线程定时从后台服务器获取数据,前台某个控件与工作线程通过交换消息来获取新的数据。

下文是Worker中的代码,接受到主线程的启动参数info后,开始获取数据并向主线程发送获取到的数据。

var info = "";

onmessage = function(event) {
info=event.data;
}

function RefreshInfo() {
if(info==""||info==undefined){
throw "no info";
}
var infoReceived = function() {
var output=httpReq.responseText;
if(output){
postMessage(output.trim());
}
httpReq=null;
}

var httpReq = new XMLHttpRequest()
var url = ""; //server url
httpReq.open("GET", url, true);
httpReq.onload =infoReceived;
httpReq.send(null);
}

setInterval(function(){
RefreshInfo();
},1000*60);

敬请期待下一篇,《JavaScript多线程之二~~Node.js中的webworker-threads》

参考文献:

  1. W3C Working Draft 08 October 2015
  2. HTML Living Standard — Last Updated 30 November 2015
  3. 解决setInterval计时器不准的问题
  4. 深入 HTML5 Web Worker 应用实践:多线程编程
  5. 使用 Web Workers
  6. Web Worker
  7. Using workers in extensions