JS中的Web Workers(一)

382 阅读6分钟

简介

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法线程可以执行任务而不干扰用户界面。此外,他们可以使用XMLHttpRequest执行 I/O (尽管responseXML和channel属性总是为空)。一旦创建, 一个worker 可以将消息发送到创建它的JavaScript代码, 通过将消息发布到该代码指定的事件处理程序(反之亦然)。

Web Worker 有以下几个使用注意点。

(1)同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

(2)DOM 限制

Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。

(3)通信联系

Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。

(4)脚本限制

Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。

(5)文件限制

Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

Web Workers API

一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的JavaScript文件,这个文件包含将在工作线程中运行的代码。需要注意的是,workers 运行在另一个全局上下文中,不同于当前的window . 因此,在 Worker 内通过 window获取全局作用域将返回错误

在workers的情况下,DedicatedWorkerGlobalScope 对象代表了worker的上下文(workers是指标准worker仅在单一脚本中被使用。

var myWorker = new Worker('worker.js');

在worker线程中你可以运行任何你喜欢的代码,不过有一些例外情况。比如:在worker内,不能直接操作DOM节点,也不能使用window对象的默认方法和属性。然而你可以使用大量window对象之下的东西,包括WebSockets,IndexedDB等数据存储机制。查看Functions and classes available to workers获取详情。

workers和主线程间的数据传递通过这样的消息机制进行——双方都使用postMessage()方法发送各自的消息,使用onmessage事件处理函数来响应消息(消息被包含在Message事件的data属性中)。这个过程中数据并不是被共享而是被复制。只要运行在同源的父页面中,workers可以依次生成新的workers;并且可以使用XMLHttpRequest 进行网络I/O,但是XMLHttpRequest的responseXML和channel属性总会返回null。

  • web worker可以做的事情:
    • 数据计算;
    • 数据请求,XHR,或fetch等(当然也可以Promise);
    • navigator(WorkerNavigator),location(WorkerLocation),setTimeout,setInterval等;
  • web worker不可做的事情:

    • 不能操作DOM节点;(无法访问document对象;)
    • 不能使用window默认方法和属性(如alert,history,localStorage等);\
  • 主线程和worker线程通信
    • 使用postMessageonmessage 来通信;
    • 消息被包含在message事件的data属性中;
    • 通信的数据不被共享,而是复制;

这个和iframe有点类似,父窗体和iframe窗体之间是相互隔离开的,但是他们都有各自的window上下文,窗体之间通信也是采用postMessage和onmessage,并且还可以跨域。当然,如果不存在跨域情况下,还可以使用dispatchEvent来派发事件进行通信。

在worker中,我们可以拿到一个DedicatedWorkerGlobalScope对象来代表worker的上下文(针对DedicatedWorker的情况)。而在主线程中,我们拿到的则是window对象。

worker中的数据接收和发送

问题:最简便的数据深拷贝的方法是什么

当然是序列化和反序列化了。

在主页面与 worker 之间传递的数据是通过拷贝,而不是共享来完成的。传递给 worker 的对象需要经过序列化,接下来在另一端还需要反序列化。页面与 worker 不会共享同一个实例,最终的结果就是在每次通信结束时生成了数据的一个副本。

线程安全

Worker接口会生成真正的操作系统级别的线程,如果你不太小心,那么并发会对你的代码产生影响。然而,对于 web worker 来说,与其他线程的通信点会被很小心的控制,这意味着你很难引起并发问题。你没有办法去访问非线程安全的组件或者是 DOM,此外你还需要通过序列化对象来与线程交互特定的数据。所以你要是不费点劲儿,还真搞不出错误来。

Dedicated Worker

一个Dedicated Worker仅仅能被生产它的脚本所使用。下面给出一个小栗子,主线程生成一个worker,在input数据变化的时候,讲数据发送给这个worker,worker内部做一些计算再将计算结果返回给主线程,主线程接收到数据,并将结果显示出来。并且,在这个worker中,还可以创建sub worker,也就是子worker。

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>Worker</title>
  </head>
  <body>
    <input id="input1" type="text"/>
    <h2 id="result"></h2>
    <script>
      let input1 = document.querySelector("#input1");
      let result = document.querySelector("#result");
      
      let myWorker = new Worker('./worker1.js');
      myWorker.onmessage = function(e){
        result.innerHTML = e.data.value;
      }
      
      input1.addEventListener("input", function(){
        myWorker.postMessage({
          value: this.value
        });
      });
    </script>
  </body>
</html>

worker1.js: 在worker内,可以直接使用onmessage,和postMessage。

onmessage = function(e){
    console.log(e.data, this.navigator, this.location, this.history, this.localStorage,this.sessionStorage);

    postMessage({
        value: e.data.value !== '' ? `Hello ${e.data.value}!` : ''
    });

    // 可以发送xhr,fetch等;
    let http = new XMLHttpRequest();
    http.onreadystatechange = function(){
        if (http.readyState === 4 && http.status === 200) {
            // console.log(http.responseText);
            console.log('读取数据成功了!');
        }
    }
    http.open('get', './worker1.js', true);
    http.send();
}


// worker中可以再创建一个worker
let myWorker2 = new Worker('./worker2.js');

worker2.js

console.log('我是第二个worker');

运行效果:

如何终止worker?

在主线程中立刻终止一个运行中的worker,可以调用terminate方法。worker 线程会被立即杀死,不会有任何机会让它完成自己的操作或清理工作。

myWorker.terminate();

而在worker线程中,worker也可以调用自己的close方法来终止。

close();

如何错误处理?
当 worker 出现运行中错误时,它的 onerror 事件处理函数会被调用。它会收到一个扩展了 ErrorEvent 接口的名为 error的事件。

如何引入脚本库?
Worker 线程能够访问一个全局函数importScripts()来引入脚本,该函数接受0个或者多个URI作为参数来引入资源;以下例子都是合法的:

importScripts();                        /* 什么都不引入 */
importScripts('foo.js');                /* 只引入 "foo.js" */
importScripts('foo.js', 'bar.js');      /* 引入两个脚本 */

浏览器加载并运行每一个列出的脚本。每个脚本中的全局对象都能够被 worker 使用。

脚本的下载顺序不固定,但执行时会按照传入 importScripts() 中的文件名顺序进行。这个过程是同步完成的;直到所有脚本都下载并运行完毕,importScripts() 才会返回。

如何生成subWorker?
如果需要的话 worker 能够生成更多的 worker。这就是所谓的subworker,它们必须托管在同源的父页面内。而且,subworker 解析 URI 时会相对于父 worker 的地址而不是自身页面的地址。这使得 worker 更容易记录它们之间的依赖关系。

官网栗子:github.com/mdn/simple-…

栗子:

<!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>Web Workers</title>
  </head>
  <body>
    <input id="input1" type="text"/>
    <div id="div1">
      
    </div>
    <script>
      const input1 = document.querySelector("#input1");
      const div1 = document.querySelector("#div1");
      
      const worker1 = new Worker("./worker1.js");
      worker1.onmessage = function(e){
        div1.innerHTML = e.data.value;
      }
      input1.addEventListener('change', function(e){
        worker1.postMessage({
          value: e.target.value
        });
      });
      
    </script>
  </body>
</html>
onmessage = function(e){
    console.log('worker内接收到了值:', e.data.value);

    postMessage({
        value: e.data.value + '666'
    });
};

let subWorker = new Worker("./sub-worker1.js");
subWorker.onmessage = function(e){
    postMessage({
        value: e.data.value
    });
};
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
  if (xhr.readyState === 4) {
    console.log("worker请求到了数据:", xhr.responseText);
    
    postMessage({
      value: xhr.responseText
    });
  }
}
xhr.open("GET", "./test.txt", true);
xhr.send();

Shared Worker

一个共享worker可以被多个脚本使用——即使这些脚本正在被不同的window、iframe或者worker访问。

注意: 如果共享worker可以被多个浏览上下文调用,所有这些浏览上下文必须属于同源(相同的协议,主机和端口号)。

DedicatedWorker VS SharedWorker

DedicatedWorker

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>Dedicated Worker</title>
</head>
<body>
    <input id="input1" type="text"/>
    <h2 id="result"></h2>
    <br/>
    <input id="input2" type="text"/>
    <h2 id="result2"></h2>

    <script>
        let myWorker = new Worker('worker1.js');
        let input1 = document.querySelector("#input1");
        let result = document.querySelector("#result");

        input1.addEventListener('change', function(e){
            console.log(e.target.value);

            myWorker.postMessage({
                value: e.target.value
            });
        });

        myWorker.onmessage = function(e){
            result.innerText = e.data.value;
        }

        // 第二个
        let myWorker2 = new Worker('worker1.js');
        let input2 = document.querySelector("#input2");
        let result2 = document.querySelector("#result2");

        input2.addEventListener('change', function(e){
            myWorker2.postMessage({
                value: e.target.value
            });
        });

        myWorker2.onmessage = function(e){
            result2.innerText = e.data.value;
        }
    </script>
</body>
</html>

worker1.js

// DedicatedWorkerGlobalScope
var count = 0;
onmessage = function(e){
    console.log("worker: ", e.data.value);

    postMessage({
        value: e.data.value ? `${e.data.value} - ${++count}` : ''
    });
}

运行效果:

DedicatedWorker的数据并不共享,不会相互影响。

SharedWorker

第一个栗子:

index2.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>Shared Worker</title>
  </head>
  <body>
    <input id="input1" type="text"/>
    <h2 id="result1"></h2>
    <br/>
    <input id="input2" type="text"/>
    <h2 id="result2"></h2>
    
    <script>
      // 第一个
      let input1 = document.querySelector("#input1");
      let result1 = document.querySelector("#result1");
      let myWorker1 = new SharedWorker('worker2.js');
      input1.addEventListener('change',function(e){
        myWorker1.port.postMessage({
          value: '第一个:' + e.target.value
        });
      });
      myWorker1.port.onmessage = function(e){
        result1.innerText = e.data.value;
      }
      
      // 第二个
      let input2 = document.querySelector("#input2");
      let result2 = document.querySelector("#result2");
      let myWorker2 = new SharedWorker('worker2.js');
      input2.addEventListener('change',function(e){
        myWorker2.port.postMessage({
          value: '第二个:' + e.target.value
        });
      });
      myWorker2.port.addEventListener('message', function(e){
        result2.innerText = e.data.value;
      });
      // addEventListener这种写法需要start一下
      myWorker2.port.start();
      
    </script>
  </body>
</html>

worker2.js

var count = 0;

onconnect = function(e){
    let port = e.ports[0];

    port.onmessage = function(e){
        port.postMessage({
            value: `${e.data.value} - ${++count}`
        });
    }
}

运行效果:

SharedWorker的数据共享,相互影响。

第二个栗子:

index2.html

let input1 = document.querySelector("#input1");
let result1 = document.querySelector("#result1");
let myWorker1 = new SharedWorker('worker2.js');
input1.addEventListener('change',function(e){
    myWorker1.port.postMessage({
        value: '第一个:' + e.target.value
    });
});
myWorker1.port.onmessage = function(e){
    result1.innerText = e.data.value;
}

index3.html

let input1 = document.querySelector("#input1");
let result1 = document.querySelector("#result1");
let myWorker1 = new SharedWorker('worker2.js');
input1.addEventListener('change',function(e){
    myWorker1.port.postMessage({
        value: '第一个:' + e.target.value
    });
});
myWorker1.port.onmessage = function(e){
    result1.innerText = e.data.value;
}

运行效果:
即使不在同一个window下,也是可以共享的。 官网栗子:github.com/mdn/simple-…

Service Worker

Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。我会在下一篇文章中详细介绍ServiceWorker。