你明知道,我知道你知道
说明
学习总结 + 个人理解,巩固 + 方便查阅,大家愿意看的简单看看就好
为什么需要 Web Worker ?
首先上一张脑图,让我们对浏览器有个大致的了解:

js 引擎是单线程的,且和 GUI 渲染线程互斥。解释:当
JS 引擎执行时 GUI 线程会被挂起,GUI 更新则会被保存在一个队列中等到 JS 引擎线程空闲时立即被执行,它们不能同时运行,因为同时运行会导致渲染出现不可预期的结果。
考虑:既然 JS 引擎线程与 GUI 渲染线程互斥,那么当 JS 执行时间过长(比如进行巨量计算),此时 GUI 渲染线程被阻塞拿不到执行权,必然导致页面渲染加载阻塞,影响用户体验,从而导致用户流量缺失。
疑问:有没有那么一种方式既能够进行计算又不影响渲染? 还有 JS 真的就对 CPU 密集型计算无能为力了吗 ?
tips: CPU 密集型、计算密集型计算,顾名思义就是应用需要非常多的CPU计算资源。
解决:Web Worker 诞生,走出困境。
对于本小节更详细的介绍可以看这篇《从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理》
Web Worker 是什么?
MDN:Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。
也就是说 Web Worker 为 JavaScript 创造了多线程环境,允许 JS 主线程创建 Worker 子线程,将一些任务分配给后者运行。这样做就能充分发挥多核 CPU 主机的优势,让两个线程并行执行,给用户带来飞一般的享受。
在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅, Worker 线程会在计算任务完成时,通过特定的通信方式将结果返回给主线程。
注意: Worker 子线程是浏览器开的,完全受主线程控制。即 Web Worker 是浏览器提供的,JS 并不提供,JS 还是单线程的。
插点题外话: 并行的前提是并发,即程序在设计的时候只有被设计成并发式的,才有能够在多核 CPU 情况下并行执行的可能。若一开始程序就被设计为同步阻塞式的即不具备并发实现,即使放在多核 CPU 环境下也不可能并行执行。并发的优势在单核 CPU 情况下不足以体现不出来,最多与同步代码执行时间相同,但它的优势将会在多核 CPU 情况下体现的淋漓尽致,所以将程序设计为并发式的只会有好处没有坏处。
Web Worker 基本用法
主线程
JS 主线程通过调用 Worker() 构造函数,新建一个 Worker 线程:
var worker = new Worker('work.js');
注意:
- 构造函数的参数是一个
url - 分配给
Worker线程运行的脚本文件,必须与主线程的脚本文件同源(协议、端口、主机相同) Worker不能读取本地的文件(不能打开本机的文件系统file://),它所加载的脚本必须来自网络
疑问: 这个 url 被限定为网络文件,我们想传入自己写的 JS 代码就无能为力了吗?
既然没有 url ,那我们就创建一个 url ,所以这样解决:
const data = `
// worker线程 do something
`;
// 转成二进制对象
const blob = new Blob([data]);
// 生成url
const url = window.URL.createObjectURL(blob);
// 加载url
const worker = new Worker(url);
objectURL = URL.createObjectURL(object);
object
用于创建URL的File对象、Blob对象或者MediaSource对象- 返回值
一个DOMString包含了一个对象URL,该URL可用于指定源object的内容
专用 Worker
一个专用Worker仅仅能被生成它的脚本所使用。var myWorker = new Worker('worker.js') 这就是专用Worker
专用Worker消息的接收和发送
在主线程和Worker内都是通过postMessage()方法发送数据,监听message事件接收数据
- 主线程内
myWorker.onmessage = function (event) { // 接收来自子线程内部的数据
console.log('Received message ' + event.data);
doSomething();
}
myWorker.postMessage('Work done!'); // 向`worker`内部发送数据
Worker子线程内
addEventListener('message', function (e) { // 接收来自主线程数据
postMessage('You said: ' + e.data); // 发送到主线程
}, false); // false指定事件句柄在冒泡阶段执行
终止 Worker
Worker 线程有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
- 从主线程中立刻终止一个运行中的
Worker:myWorker.terminate(); - 在
Worker线程中自行终止:self.close();(self代表子线程自身)
注意: Worker 线程关闭就会被立即杀死,不会有任何机会让它完成自己的操作或清理工作,理解为秒杀就 OK 啦。
Worker 加载脚本
Worker 内部如果要加载其他脚本,有一个专门的方法importScripts(),它可以接收一个或多个参数:
importScripts('script1.js', 'script2.js');
错误处理
在主线程和Worker内都通过监听 error 事件来捕获错误:
myWorker.onerror(function (event) {
// 做处理
});
addEventListener('error', function (event) {
// 做处理
})
回调函数参数 event 的属性:
message: 可读性良好的错误消息filename: 发生错误的脚本文件名lineno: 发生错误时所在脚本文件的行号
Web Worker 的注意点
(1)同源限制
分配给Worker线程运行的脚本文件,必须与主线程的脚本文件同源。
(2)DOM限制
Worker线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的DOM对象,也无法使用document、window、parent这些对象。但是,Worker线程可以navigator对象和location对象。
(3)脚本限制
Worker线程不能执行alert()方法和confirm()方法,但可以使用XMLHttpRequest对象发出AJAX请求。
(4)Worker内部还可以新建Worker,但只有Firefox浏览器支持。
模拟 Web Worker
由于环境所限,你可能需要在缺乏对此支持的更老的浏览器中运行代码。因为 Worker 是一种 API 而不是语法,所以我们可以作为扩展来模拟它。
如果浏览器不支持
Worker,那么从性能的角度来说是没法模拟多线程的。通常认为Iframe提供了并行环境,但是在所有的现代浏览器中,它们实际上都是和主页面运行在同一个线程中的,所以并不足以模拟Worker。
JavaScript 的异步(不是并行)来自于事件循环队列,所以可使用定时器(setTimeout(..) 等)强制模拟实现异步的伪 Worker,然后你只需要提供一个 Worker API 的封装。当然最稳妥的方式还是找一个已经成熟的第三方库。
关于 Iframe:
优点:
- 能够原封不动的把嵌入的网页展现出来
- 如果有多个网页引用
iframe,那么你只需要修改iframe的内容,就可以实现调用的每一个页面内容的更改,方便快捷 - 可以用来解决加载缓慢的第三方内容如图标和广告问题
缺点:
- 用户体验度差,代码复杂,不利于搜索引擎优化(
SEO) - 设备兼容性差,很多的移动设备无法完全显示框架
- 会增加服务器的
http请求,影响性能 - 阻塞主页面的
Onload事件 - 和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载
- 比其它包括
scripts和css的DOM元素的创建慢1-2个数量级
注意: 主页面和其中的 iframe 共享连接池,这个连接池可以理解为浏览器当前打开页面到web服务器一个域名同时可打开的连接数(2-6个),如果 iframe 在加载资源时用光了所有的可用连接,那么主页面资源的加载就会被阻塞,这也解释了上面的观点(iframe不足以模拟web worker)。所以要谨慎的使用 iframe,因为它会极大地影响性能。
共享 Worker—— SharedWorker
SharedWorker 是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render 进程,可以为多个Render进程共享使用。
生成共享 Worker:
var myWorker = new SharedWorker('worker.js');
共享Worker通信必须通过端口对象,在传递消息之前,端口连接必须被显式的打开,打开方式是使用 start() 方法:
myWorker.port.start(); // 父级线程中的调用
port.start(); // worker线程中的调用, 假设port变量代表一个端口
共享worker中消息的接收和发送必须通过端口对象调用postMessage() 方法:
myWorker.port.postMessage(data); // 主线程
// worker 子线程,当一个端口连接被创建时,使用onconnect事件处理函数来执行代码
onconnect = function(e) {
var port = e.ports[0];
port.onmessage = function(e) {
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
port.postMessage(workerResult);
}
}
注意: SharedWorker 目前 IE 、 Safari 及移动端浏览器不支持; Worker 除 IE10 以下不支持外,基本都支持。