JS"虚假"的多线程

567 阅读5分钟

WEB Worker

以前我们总说,JS 是单线程没有多线程,当 JS 在页面中运行长耗时同步任务的时候就会导致页面假死影响用户体验,从而需要设置把任务放在任务队列中;执行任务队列中的任务也并非多线程进行的,然而现在 HTML5 提供了我们前端开发这样的能力 - Web Workers API,我们一起来看一看 Web Worker 是什么,怎么去使用它,在实际生产中如何去用它来进行产出。

概述

Web Workers 使得一个 Web 应用程序可以在与主执行线程分离的后台线程中运行一个脚本操作。这样做的好处是可以在一个单独的线程中执行费时的处理任务,从而允许主(通常是 UI)线程运行而不被阻塞。 它的作用就是给 JS 创造多线程运行环境,允许主线程创建 worker 线程,分配任务给后者,主线程运行的同时 worker 线程也在运行,相互不干扰,在 worker 线程运行结束后把结果返回给主线程。这样做的好处是主线程可以把计算密集型或高延迟的任务交给 worker 线程执行,这样主线程就会变得轻松,不会被阻塞或拖慢这并不意味着 JS 语言本身支持了多线程能力,而是浏览器作为宿主环境提供了 JS 一个多线程运行的环境

不过因为 worker 一旦新建,就会一直运行,不会被主线程的活动打断,这样有利于随时响应主线程的通性,但是也会造成资源的浪费,所以不应过度使用,用完注意关闭。或者说:如果 worker 无实例引用,该 worker 空闲后立即会被关闭;如果 worker 实列引用不为 0,该 worker 空闲也不会被关闭。

看一看它的兼容性

BrowserIEEDGEFoxChromeSafari
version10+12+3.5+4+4+

使用

worker 线程的使用有一些注意点

  1. 同源限制 worker 线程执行的脚本文件必须和主线程的脚本文件同源,这是当然的了,总不能允许 worker 线程到别人电脑上到处读文件吧
  2. 文件限制 为了安全,worker 线程无法读取本地文件,它所加载的脚本必须来自网络,且需要与主线程的脚本同源
  3. DOM 操作限制 worker 线程在与主线程的 window 不同的另一个全局上下文中运行,其中无法读取主线程所在网页的 DOM 对象,也不能获取 document、window 等对象,但是可以获取 navigator、location(只读)、XMLHttpRequest、setTimeout 族等浏览器 API。
  4. 通信限制 worker 线程与主线程不在同一个上下文,不能直接通信,需要通过 postMessage 方法来通信。
  5. 脚本限制 worker 线程不能执行 alert、confirm,但可以使用 XMLHttpRequest 对象发出 ajax 请求。

例子

var myWorker = new Worker(jsUrl, options);

Worker()构造函数,第一个参数是脚本的网址(必须遵守同源政策),该参数是必需的,且只能加载 JS 脚本,否则报错。第二个参数是配置对象,该对象可选。它的一个作用就是指定 Worker 的名称,用来区分多个 Worker 线程。

// 主线程
var myWorker = new Worker("worker.js", { name: "myWorker" });
// Worker 线程
self.name; // myWorker

主线程中的 api,worker 表示是 Worker 的实例:

  • worker.postMessage: 主线程往 worker 线程发消息,消息可以是任意类型数据,包括二进制数据
  • worker.terminate: 主线程关闭 worker 线程
  • worker.onmessage: 指定 worker 线程发消息时的回调,也可以通过 worker.addEventListener('message',cb)的方式
  • worker.onerror: 指定 worker 线程发生错误时的回调,也可以 worker.addEventListener('error',cb)

Worker 线程中全局对象为 self,代表子线程自身,这时 this 指向 self,其上有一些 api:

  • self.postMessage: worker 线程往主线程发消息,消息可以是任意类型数据,包括二进制数据
  • self.close: worker 线程关闭自己
  • self.onmessage: 指定主线程发 worker 线程消息时的回调,也可以 self.addEventListener('message',cb)
  • self.onerror: 指定 worker 线程发生错误时的回调,也可以 self.addEventListener('error',cb)

场景

个人觉得,Web Worker 我们可以当做计算器来用,需要用的时候掏出来摁一摁,不用的时候一定要收起来~

  1. 加密数据 有些加解密的算法比较复杂,或者在加解密很多数据的时候,这会非常耗费计算资源,导致 UI 线程无响应,因此这是使用 Web Worker 的好时机,使用 Worker 线程可以让用户更加无缝的操作 UI。

  2. 预取数据 有时候为了提升数据加载速度,可以提前使用 Worker 线程获取数据,因为 Worker 线程是可以是用 XMLHttpRequest 的。

  3. 预渲染 在某些渲染场景下,比如渲染复杂的 canvas 的时候需要计算的效果比如反射、折射、光影、材料等,这些计算的逻辑可以使用 Worker 线程来执行,也可以使用多个 Worker 线程;

  4. 复杂数据处理场景 某些检索、排序、过滤、分析会非常耗费时间,这时可以使用 Web Worker 来进行,不占用主线程。

  5. 预加载图片 有时候一个页面有很多图片,或者有几个很大的图片的时候,如果业务限制不考虑懒加载,也可以使用 Web Worker 来加载图片,可以参考一下这篇文章的探索,这里简单提要一下。

// 主线程
let w = new Worker("js/workers.js");
w.onmessage = function (event) {
  var img = document.createElement("img");
  img.src = window.URL.createObjectURL(event.data);
  document.querySelector("#result").appendChild(img);
};

// worker线程
let arr = [...好多图片路径];
for (let i = 0, len = arr.length; i < len; i++) {
  let req = new XMLHttpRequest();
  req.open("GET", arr[i], true);
  req.responseType = "blob";
  req.setRequestHeader("client_type", "DESKTOP_WEB");
  req.onreadystatechange = () => {
    if (req.readyState == 4) {
      postMessage(req.response);
    }
  };
  req.send(null);
}

在实战的时候注意

  • 虽然使用 worker 线程不会占用主线程,但是启动 worker 会比较耗费资源
  • 主线程中使用 XMLHttpRequest 在请求过程中浏览器另开了一个异步 http 请求线程,但是交互过程中还是要消耗主线程资源