"JavaScript是单线程的",Js的开发这们一直这样认为。但是今天要介绍的是JavaScript中的多线程编程,你没有听错,我说的就是多线程编程——Web Workers。
Web Workers是浏览器(宿主环境)的一个功能,并不是JavaScript的标准功能,那么它有什么作用呢?
假设你在页面上点击一个按钮,然后会触发执行一系列密集型任务,需要耗费几十秒甚至几分钟执行完毕,那么此时UI会处于卡死状态,导致Page Unresponsive错误。你肯定不希望这种事情发生,这会导致很不好的用户体验。
你希望有一种方式,可以让主线程继续渲染UI,而将密集型任务的处理交给另一个子线程取负责,等子线程处理完毕后,主线程可以获得子线程的处理结果并作出反应。这就是Web Workers出现的意义。
你可以在主线程创建一个Web worker,将一些耗时的任务交给Web worker处理,Web worker处理任务完毕,将结果返回给主线程即可。这样就不会导致Page Unresponsive这种情况出现。
Web Workers机制与C++、C#其他语言中的多线程不同的是子线程(Web worker)不能与其他线程(主线程)共享内存。这是一个优点,共享内存意味着共享作用域和资源,如果共享内存,那么你将遇到其他多线程语言(C++、C#等)要面对的所有问题,比如合作式或抢占式的锁机制(mutex等),那就太麻烦了。
Worker线程一旦新建成功,它会始终运行,所以在使用完毕后,就应该关闭,否则占用资源过度。
Web Worker有以下几个使用注意点。
(1)同源限制
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
(2)DOM 限制
Worker线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的DOM
对象,也无法使用document
、window
、parent
这些对象。但是,Worker
线程可以读取navigator
对象和location
对象。
(3)通信联系
Worker线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
(4)脚本限制
Worker线程不能执行alert()
方法和confirm()
方法,但可以使用XMLHttpRequest
对象发出AJAX
请求。
(5)文件限制
Worker
线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
在主线程内新建Worker线程。
var worker = new Worker('worker.js');
Worker()
构造函数的参数是一个脚本文件,脚本文件是Worker线程的执行代码。
主线程与Worker通过message通信,调用worker.postMessage()
方法向Worker发送消息,worker在接收到主线程的消息,可以根据消息内容处理任务(具体如何根据消息处理任务,在后面会讲解)。
worker.postMessage({data: "Web worker tutorial"});
postMessage()
的参数可以是任意类型的数据,包括二进制。
主线程可以通过worker监听message
事件接收子线程的消息,通过event.data
获取数据,与其他事件处理方式类似。
有两种方式,onmessage()
或者addEventListener()
。
(1)onmessage()
worker.onmessage = receivedWorkerMessage;
function receivedWorkerMessage(event) {
var message = event.data; // 消息内容
...
}
(2)addEventListener()
worker.addEventListener("message", receivedWorkerMessage);
function receivedWorkerMessage(event) {
var message = event.data; // 消息内容
...
}
Worker完成任务后,主线程通过worker.terminate()
关闭worker。
主线程通过worker.postMessage
触发message
事件,向worker线程发送消息。Worker通过监听message
事件,可以接受主线程发送的消息。
子线程中的全局对象为self
关键字,不是window
。worker的message
事件与其他dom事件一样,所以可以有几种方式监听事件。
(1)使用self.addEventListener()
。
self.addEventListener('message', function (event) {
const message = event.data;
...根据消息内容,进行一系列任务(通常是耗时的任务)
};, false);
// self为全局对象所以可以省略
addEventListener('message', function (event) {
const message = event.data;
...根据消息内容,进行一系列任务(通常是耗时的任务)
};, false);
(2)使用self.onmessage()
。
self.onmessage = function (event) {
const message = event.data;
...根据消息内容,进行一系列任务(通常是耗时的任务)
};
// self为全局对象所以可以省略
self.onmessage = function (event) {
const message = event.data;
...根据消息内容,进行一系列任务(通常是耗时的任务)
};
Worker可以通过importScripts()
加载脚本。
// 加载单个脚本
importScripts('script1.js');
// 加载多个脚本
importScripts('script1.js', 'script2.js');
主线程可以监听错误,如果发生错误,Worker会触发error
事件。
(1)worker.onerror()
。
worker.onerror = workerError;
function workerError(error) {
...
}
(2)worker.addEventListener
。
worker.addEventListener("error", workerError);
function workerError(error) {
...
}
// 主线程
worker.terminate();
// Worker 线程
self.close(); // self可以省略
下面是一个在指定数字范围内,查找所有的素数的demo,比较耗费时间,所以查找计算的过程放到了Worker中处理。你可以点击Start Searching按钮查看运行效果。
附:参考资料
(完)