你还没有用过Web Workers?

223 阅读5分钟

单线程背景

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更具体的认识。

如果觉得本文对您有一点帮助的话,麻烦给我点个赞吧~

参考