背景简述

JavaScript作为浏览器脚本语言,被定义为只有单线程的语言,也就是同一时间只能做同一事情。
当JS在页面中运行长耗时同步任务的时候就会导致页面假死影响用户体验,从而需要设置把任务放在任务队列中;执行任务队列中的任务也并非多线程进行的。
在HTML5中引入了webWorker的概念,为JavaScript引入了线程的概念,它允许开发人员编写能够长时间运行而不被用户所中断的后台程序,去执行事务或者逻辑,并同时保证页面对用户的响应。但是子线程完全受主线程控制,且不得修改DOM。所以一般情况下webWorker的主要用途是处理一些比较耗时的计算。

webworker

webWorker注意点

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

由于 Web Workers 的多线程特性,它只能使用一部分 JavaScript 功能。以下是可使用的功能列表:

1.navigator 对象
2.location 对象(只读)
3.XMLHttpRequest
4.setTimeout()/clearTimeout() 和 setInterval()/clearInterval()
5.Application Cache
6.使用 importScripts 来引用外部脚本
7.创建其它 web workers
Web Worker 的局限性

Web Workers 不能够访问一些非常关键的 JavaScript 功能:

1.DOM(非线程安全的)
2.window 对象
3.document 对象
4.parent 对象

常用api

详细的API参见 MDN - WorkerGlobalScope

postMessage(data)

子线程与主线程之间互相通信使用方法,传递的data为任意值。

//worker = new Worker('url');
//worker.postMessage传递给子线程数据,对象
worker.postMessage({first:1,second:2});

//子线程中也可以使用postMessage,如传递字符串
postMessage(‘test’);
terminate()

主线程中终止worker,此后无法再利用其进行消息传递。注意:一旦terminate后,无法重新启用,只能另外创建。

//worker = new Worker('url');
worker.terminate();
message

当有消息发送时,触发该事件。且,消息发送是双向的,消息内容可通过data来获取。

worker.onmessage = function(event){
    console.log(event.data);
};
error

出错处理。且错误消息可以通过e.message来获取。

//worker = new Worker('url');
worker.onerror = function(e){
    //打印出错消息
    console.log(e.message);
    //中断与子线程的联系
    worker.terminate();
}

注:worker线程从上到下同步运行它的代码,然后进入异步阶段来对事件及计时器响应,如果worker注册了message事件处理程序,只要其有可能触发,worker就一直在内存中,不会退出,所以通信完毕后得手动在主线程中terminate或者子线程中close掉,但如果worker没有监听消息,那么当所有任务执行完毕(包括计数器)后,他就会退出。
self

我们可以使用 WorkerGlobalScope 的 self 属性来或者这个对象本身的引用

location

location 属性返回当线程被创建出来的时候与之关联的 WorkerLocation 对象,它表示用于初始化这个工作线程的脚步资源的绝对 URL,即使页面被多次重定向后,这个 URL 资源位置也不会改变。

close

关闭当前线程,与terminate作用类似

importScripts

我们可以通过importScripts()方法通过url在worker中加载库函数

XMLHttpRequest

发Ajax请求

setTimeout/setInterval

延时执行函数和定时执行函数,和window对象的方法相同。

addEventListener/postMessage

第一个为注册监听事件,和window对象的相同,不在赘述。postMessage上面也介绍过了,是主线程和子线程之间通信的方法。

小结
主线程

浏览器原生提供Worker()构造函数,用来供主线程生成 Worker 线程。

    var myWorker = new Worker(jsUrl, options);

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

Worker()构造函数返回一个 Worker 线程对象,用来供主线程操作 Worker。Worker线程对象的属性和方法如下。

    Worker.onerror:指定 error 事件的监听函数。
    Worker.onmessage:指定 message 事件的监听函数,发送过来的数据在Event.data属性中。
    Worker.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
    Worker.postMessage():向 Worker 线程发送消息。
    Worker.terminate():立即终止 Worker 线程。
Worker 线程

Web Worker 有自己的全局对象,不是主线程的window,而是一个专门为 Worker 定制的全局对象。因此定义在window上面的对象和方法不是全部都可以使用。

    self.name: Worker 的名字。该属性只读,由构造函数指定。
    self.onmessage:指定message事件的监听函数。
    self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
    self.close():关闭 Worker 线程。
    self.postMessage():向产生这个 Worker 线程发送消息。
    self.importScripts():加载 JS 脚本。

demo

index.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>test web worker</title>
    </head>
    <body>
        <button onclick="startComputation()">Start computation</button>

    </body>
    <script>
        function startComputation() {
        var arr = new Array(20000000).fill(1)
        worker.postMessage({'cmd': 'average', 'data': arr});
        }
        var worker = new Worker('doWork.js');
        worker.addEventListener('message', function(e) {
        console.log(e.data);
        }, false);
    </script>
    </html>

doWork.js

    self.addEventListener('message', async function(e) {
        var data = e.data;
        switch (data.cmd) {
        case 'average':
            var result = await calculateAverage(data.data); // 某个数值数组中计算平均值的函数
            self.postMessage(result);
            break;
        default:
            self.postMessage('Unknown command');
        }
    }, false)
    
    function calculateAverage(numbers) {
        var len = numbers.length,
            sum = 0,
            i;

        if (len === 0) {
            return 0;
        } 
        
        for (i = 0; i < len; i++) {
            sum += numbers[i];
        }
        return sum / len;
    }

使用场景

How JavaScript works: The building blocks of Web Workers + 5 cases when you should use them

计算密集型或高延迟的任务可用web worker处理,避免主线程(UI)阻塞。

加密数据

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

预取数据

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

预渲染

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

复杂数据处理场景

某些检索、排序、过滤、分析会非常耗费时间,这时可以使用Web Worker来进行,不占用主线程。

预加载图片

有时候一个页面有很多图片,或者有几个很大的图片的时候,如果业务限制不考虑懒加载,也可以使用Web Worker来加载图片。

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

浏览器需要轮询服务器状态,以便第一时间得知状态改变。这个工作可以放在 Worker 里面。

function createWorker(f) {
var blob = new Blob(['(' + f.toString() +')()']);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
return worker;
}

var pollingWorker = createWorker(function (e) {
var cache;

function compare(new, old) { ... };

setInterval(function () {
    fetch('/my-api-endpoint').then(function (res) {
    var data = res.json();

    if (!compare(data, cache)) {
        cache = data;
        self.postMessage(data);
    }
    })
}, 1000)
});

pollingWorker.onmessage = function () {
// render data
}

pollingWorker.postMessage('init');

好文推荐:Web Worker