单线程背景
JavaScript从语言设计上就注定了它单线程运行的机制。这在早期设计之初是为了避免重复修改DOM,而导致渲染混乱的问题。但在如今多核CPU普及使用,以及JavaScript需要处理大量纯逻辑计算的背景下,如何通过进一步利用CPU的性能来提升程序的体验成了业内的一个问题。
Web Workers的诞生
在这个背景下,Web Workers应运而生。它可以在Javascript主线程下生成一个新的JavaScript运行环境,该环境与主线程完全独立互不干扰,同时又可以通过专门的通信机制实现两者之间的数据沟通。这样开发者就可以把一些计算密集型或高延迟的任务交给Web Workers来做,而避免主线程的阻塞了。
非完整环境
虽然在Web Workers环境中,JavaScript代码看似跟主线程中一样,但实际上Web Workers有着独特的环境限制:
- 同源限制: 分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
- DOM 限制: Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。
- 通信联系: Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
- 脚本限制: Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
- 文件限制: Worker 线程无法读取本地文件,即不能打开本机的文件系统,它所加载的脚本,必须来自网络。
使用
Web Workers的使用方法很简单只要new 一个Worker实例即可。
var worker = new Worker('work.js');
如果需要做兼容判断,在使用Web Workers之前可以通过window.Worker先判断当前环境支不支持。
if (window.Worker) {
var worker = new Worker('work.js');
}
通信方式
Web Workers与主线程或者其他workers之间的通过postMessage通信
// index.html
const worker = new Worker('./worker.js');
worker.postMessage('msg'); // 主线程向worker发消息
// worker.js
// worker接受消息
self.onmessage=(event)=>{
console.log(event.data); // 'msg'
};
同理,如果是worker向主线程发消息
// index.html
const worker = new Worker('./worker.js');
// 接受消息
worker.onmessage=(event)=>{
console.log(event.data);
};
// worker.js
self.postMessage('来自worker的消息'); // 向主线程发消息
值得注意的是通过postMessage发送的消息底层是一个拷贝机制而不是引用,因此别的线程是无法直接修改来自消息的源数据的。
// index.html
const worker = new Worker('./worker.js');
const obj = {text:'main'};
// 接受消息
worker.onmessage=(event)=>{
console.log(event.data);
console.log(obj);
};
worker.postMessage(obj);
// worker.js
self.onmessage = (event) => {
const obj = event.data;
obj.text = "worker";
self.postMessage(obj);
};
运行上面的代码会发现虽然来自worker的数据text已经修改了,但ob对象本身并没有修改。如果想要修改obj,需要主动执行obj=event.data。
二进制文件处理
Web Worker的使用场景当然不局限于数据计算,文件处理对多线程运算需要同样很大。因此Web Workers也支持二进制数据的传递(如 File、Blob、ArrayBuffer 等类型)。
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
worker.postMessage(uInt8Array);
但前面我们也讲到了postMessage底层通过拷贝传递数据。这样的话,当我们在处理大文件时就会出现性能问题。如需要处理几百M或者上G的文件时,浏览器会先拷贝一份,这里涉及到的性能开销可想而知。为了解决这个问题,我们可以使用Transferable,它可以让浏览器直接把二进制文件从主线程转移到worker中,而不需要拷贝。这样就可以极大提高性能。但必须要注意,当文件被转移后主线程便无法继续使用,开发者在开发时应该避免继续操作。
其他操作
除了postMessage与onmessage之外,worker还有下列属性和API:
- self.name: Worker 的名字。该属性只读,由构造函数指定。
- self.onmessage:指定message事件的监听函数。
- self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
- self.close():关闭 Worker 线程。
- self.postMessage():向产生这个 Worker 线程发送消息。
- self.importScripts():加载 JS 脚本。
而对于主线程的worker实例则有:
- Worker.onerror:指定 error 事件的监听函数。
- Worker.onmessage:指定 message 事件的监听函数,发送过来的数据在Event.data属性中。
- Worker.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
- Worker.postMessage():向 Worker 线程发送消息。
- Worker.terminate():立即终止 Worker 线程。
资源释放
Web Workers虽然在某程度上极大地提高了程序的运行性能,但本质上我们仍是在开销用户的硬件成本。因此为了保证用户有良好的体验,在每次使用完Web Workers之后我们都应该要即使销毁worker实例,保证资源的回收。
// 在主线程中
worker.terminate();
// worker线程中可以直接自我销毁
self.close();
应用场景
- 服务器轮询: 在一些场景下客户端需要定时轮询访问服务器,以获得最新的数据。类似这种操作就可以放在Web Workers中完成。
- 音视频解码: 某些应用会要求在客户端实现部分文件处理。当文件太大或者处理复杂的时候,如果直接使用主线程就会导致应用出现卡顿。因此Web Worker也成了不错的选择。
- 高密度计算: 指量级足以使Javascript线程阻塞的纯逻辑计算,也可以让它从主线程中分离出来。
总结
今天我们了解了什么是Web Workers,以及他诞生的背景。然后讲述了他的优势和使用方法,并顺便讲述了部分相关的使用窍门。最后我们提到了一些真正可以落地的应用场景,以给大家对Web Workers更具体的认识。
如果觉得本文对您有一点帮助的话,麻烦给我点个赞吧~