[TOC]

在使用 HTML 4 与 JavaScript 创建出来的 Web 程序中,因为所有的处理都是在单线程内执行的,所以如果花费的时间比较长的话,程序界面会处于长时间没有响应的状态。

为了解决这个问题,在 html5 新增了 Web Workers API,使得用户可以创建后台线程(html5 中称为 workers),让耗费时间的处理交给后台去执行。

基础知识

创建后台线程的步骤很简单,只需在 Worker 类的构造器中,将需要在后台线程中执行的脚本文件的 URL 地址作为参数,然后创建 Worker 对象就可以了。

后台线程中是不能访问页面或窗口对象的。如果在后台线程的脚本文件中使用到 window 对象或 document 对象,会引发错误。
所以,可以通过对 Worker 对象的 onmessage 事件句柄的获取可以在后台线程中接收消息,通过 Worker 对象的 postMessage 方法来对后台线程发送消息。

在该示例中,放置了一个文本框,用户在该文本框中输入数字,然后点击旁边的计算按钮,后台计算从 1 到给定数值的合计值。

<!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>Document</title>
</head>
<body>
  <div>
    <p>从1到给定数值的求和示例</p>
    输入数值: <input type="text" id="num">
    <button onclick="calculate()">计算</button>
  </div>
</body>
</html>

<script>

  // 创建执行运算的线程
  var worker = new Worker('worker.js')
  // 接收从线程中传出的计算结果
  worker.onmessage = function (event) {
    // console.log('线程源的 event', event);
    // 消息文本放置在data属性中,可以是任何js对象
    console.log('合计值为' + event.data);
  }

  function calculate() {
    var num = parseInt(document.getElementById('num').value, 10);
    // 将数值传给线程
    worker.postMessage(num);
  }
</script>
// worker.js

// 线程内通过onmessage接收线程创建源发送的消息
onmessage = function (event) {
    // console.log("线程内容的 event", event);

    var num = event.data;
    var result = 0;
    for (let i = 0; i < num; i++) {
        result += i;
    }
    // 向线程创建源送回(发送)消息
    postMessage(result);
};

image.png

线程嵌套

单层嵌套

<!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>Document</title>
</head>
<body>

</body>
</html>
<script>

  var worker = new Worker('./worker.js');
  worker.postMessage("页面发送数据");

  worker.onmessage = function(event) {
    console.log('接受来自子线程2的返回的数据',event.data);
  }
</script>
// worker.js

onmessage = function (event) {
    var worker1 = new Worker("./worker1.js");
    worker1.postMessage("主线程发送消息");

    worker1.onmessage = function (event) {
        postMessage(event.data);
    };
};
// worker1.js

onmessage = function (event) {
    // 创建一个随机值并返回
    var random = Math.random();
    postMessage(random);
};

在这个例子中,向子线程提交消息时使用的是 worker2.postMessage(),而向主页面提交消息时使用的是 postMessage()

:::info 在线程中,向子线程提交消息时使用子线程对象的 postMessage 方法,而向本线程的创建源发送消息时直接使用 postMessage 方法。 :::

多个子线程中进行数据的交互

要实现子线程与子线程之间的数据交互,大致需要如下几个步骤:

  1. 先创建发送数据的子线程 worker1;
  2. 执行子线程 worker1 中任务,然后把要传递的数据发送给主线程 worker;
  3. 在主线程 worker 接收到子线程 worker1 传回来的消息时,创建接收数据的子线程 worker2,然后把子线程 worker1 中返回的消息传递给子线程 worker2;
  4. 执行接收数据子线程 worker2 中的代码。 ```html <!DOCTYPE html>

```javascript
// worker.js

onmessage = function (event) {
    var worker1 = new Worker("./worker1.js");
    worker1.postMessage("");

    worker1.onmessage = function (event) {
        var worker2 = new Worker("./worker2.js");
        worker2.postMessage(event.data);
        worker2.onmessage = function (event2) {
            postMessage(event2.data);
        };
    };
};
// worker1.js

onmessage = function (event) {
    // 创建一个随机值并返回
    var random = Math.random();
    postMessage(random);

     // 关闭子线程
  close();
};
// worker2.js

onmessage = function (event) {
    // 随机值乘以10返回
    var result = event.data * 10;
    postMessage(result);

  close();
};

线程中可用的变量、函数与类

因为 Web Worker 属于一种被后台执行的线程,所以在后台线程中只能使用 JavaScript 脚本文件中的部分对象与方法。如下所示。

  • self:用来表示本线程范围内的作用域;
  • postMessage(message):用于向创建线程的源窗口发送消息;
  • onmessage:获取接收消息的事件句柄;
  • importScripts(urls):导入其他 JavaScript 脚本文件。参数为该脚本文件的 url 地址,可以导入多个脚本文件,如 importSrcripts('script1.js', 'script/script3.js');。另外,导入的脚本文件必须与使用该线程文件的页面在同一个域中,且在同一个端口中;
  • navigator:与 window.navigator 对象类似,具有 appName,platform,userAgent,appVersion 属性
  • sessionStorage/localStorage:可以在线程中使用 Web Storage;
  • XMLHttpRequest:可以在线程中处理 Ajax 请求;
  • Web Workers:可以在线程中嵌套线程;
  • setTimeout()/setInterval():可以在线程中实现定时处理;
  • close:用于结束本线程;
  • eval(),isNaN(),escape() 等可以使用所有 JavaScript 核心函数;
  • object:可以创建和使用本地对象;
  • WebSockets(后续介绍):可以使用 WebSockets API 来向服务器发送和接收消息。

适用场景

Web Workers API 适用于如下所示的一些场合:

  • 预先抓取并缓存一些数据以供后期使用;
  • 代码高亮处理或其他一些页面上的文字格式化处理;
  • 拼写检查;
  • 分析视频或音频数据;
  • 后台 I/O 处理;
  • 大数据量分析或计算处理;
  • canvas 元素中的图像数据的运算及生成处理(后续介绍);
  • 本地数据库中数据的存取及计算处理;

SharedWorker

基础知识

html5 中除了 Web Worker 外,还有一种叫 SharedWorker。通过 SharedWorker 的使用,多个页面可以共用一个后台线程,后台线程可以作为一个提供后台服务的场所。

创建一个 SharedWorker 对象如下所示。url 指定后台脚本文件的 URL 地址;name 指定 Worker 的名称,可选参数。

var worker = new SharedWorker(url, [name]);

当创建多个 SharedWorker 对象时,程序将根据参数值来判断是否创建不同的线程。

<!-- Chrome 6以上版本、Firefox 29以上版本、IE 10、Opera 10.60以上版本的浏览器支持 -->

// 以下两个ShareWorker对象共享一个后台线程
var worker1 = new SharedWorker('srcipt1.js');
var worker2 = new SharedWorker('srcipt1.js');
// 以下两个ShareWorker 对象创建不同的后台线程
var worker = new SharedWorker('srcipt1.js');
var worker = new SharedWorker('srcipt2.js');
// 以下两个ShareWorker对象共享一个后台线程
var worker1 = new SharedWorker('srcipt1.js', 'name1');
var worker2 = new SharedWorker('srcipt1.js', 'name1');
// 以下两个ShareWorker 对象创建不同的后台线程
var worker = new SharedWorker('srcipt1.js', 'name1');
var worker = new SharedWorker('srcipt1.js', 'name2');

实现前台页面与后台线程间的通讯

当 SharedWorker 对象被创建时,一个 MessagePort 对象也同时被创建,可以通过 SharedWorker 对象的 port 属性值来访问该对象,该对象代表页面通信时需要使用的端口,具有如下三个方法:

  • postMessage:用于向另一个页面发送消息;
  • start:用于激活端口,开始监听端口是否接收到消息;
  • close:用于关闭并停用端口;

可以通过 MessagePort 对象的 postMessage 方法从端口向共享的后台线程发送消息。
可以通过监听 MessagePort 对象的 onmessage 事件并指定事件处理函数的方法来指定在该端口接收到消息时所做的处理。
也可以通过 MessagePort 对象的 addEventListener 方法来监听 message 事件的触发,并指定事件触发时所要执行的事件处理函数。但这种情况下,必须通过 start 方法来显式地激活端口,开始消息的监听。

var worker = new SharedWorker('./script.js');
var port = worker.port;
port.postMessage(message);

port.onmessage = function() {
    // 处理收到的消息 
}

port.addEventListener('message', function(event) {
    // 处理收到的消息
}, false);
port.start();

定义页面与共享的后台线程开始通信时的处理

当某个页面通过 SharedWorker 对象与共享后台线程开始通信时,会触发后台线程对象的 connect 事件,我们可以监听该事件并且在后台脚本文件中定义该事件触发时所做的处理,如下所示。

onconnect = function(event) {
    // 定义事件处理函数
}

event 的 port 属性值为一个集合,第一个参数是该页面中的 SharedWorker 对象的 port 属性值(即该页面用于发送或获取消息的端口的 MessagePort 对象)。我们可以通过脚本程序在该端口接收到消息时使用该端口对象的 postMessage 方法向页面发送一些应答消息,该页面的脚本程序将通过该端口对象的 message 事件处理函数来执行接收到消息时所做的后续处理。

onconnect = function(event) {
    // 取得页面上的SharedWorker对象的端口
    var port = event.ports[0];
  // 定义端口接收到消息时的事件处理函数
  port.onmessage = function(event) {
      // 向页面返回接收到的消息
    port.portMessage(event.data);
  }
}

SharedWorker使用示例

在单个页面中使用SharedWorker

当页面打开时即通过一个 SharedWorker 对象创建一个后台线程,从该后台线程中取得“你好”消息文字并将其显示在页面上。

<!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>Document</title>
</head>
<body onload="window_onload();">
  <div id="div1"></div>
</body>
</html>
<script>
  function window_onload() {
    var worker = new SharedWorker('sharedworker.js');
    var div = document.getElementById('div1');

    // 指定在该端口上的事件处理函数,这里是将接收到的消息输出到div中
    worker.port.onmessage = function(event) {
      div.innerHTML = event.data;
    }
  }
</script>
// sharedworker.js

// 通讯时触发onconnect事件
onconnect = function (event) {
  // event.ports的第一项获取到的是页面上的 SharedWorker 对象的 port 属性值
    var port = event.ports[0];
  // 向页面发送消息
    port.postMessage("你好");
};

在多个页面中使用SharedWorker

示例中使用两个示例页面,这两个示例页面共享同一个后台线程。当第一个示例页面打开时即通过一个 SharedWorker 对象创建一个后台线程,页面打开时通过端口向后台线程发送数字1,从后台线程中获取数字 1 的平方值 1 并将其显示在页面上;当第二个示例页面打开时即通过一个 SharedWorker 对象共享第一个示例页面中创建的后台线程,页面打开时通过端口向后台线程发送数字2,从后台线程中获取数字 2 的平方值 4 并将其显示在页面上。

<!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>Document</title>
</head>
<body onload="window_onload();">
  <div id="div1"></div>
</body>
</html>
<script>
  var worker;
  function window_onload() {
    worker = new SharedWorker('sharedworker.js');
    var div = document.getElementById('div1');
    var port = worker.port;
    port.addEventListener('message', function(event) {
      div.innerHTML = event.data;
    }, false);
    port.start();
    port.postMessage(1);
  }
</script>
<!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>Document</title>
</head>
<body onload="window_onload();">
  <div id="div2"></div>
</body>
</html>
<script>
  var worker;
  function window_onload() {
    worker = new SharedWorker('sharedworker.js');
    var div = document.getElementById('div2');
    var port = worker.port;
    port.addEventListener('message', function(event) { // port上定义事件处理函数
      div.innerHTML = event.data;
    }, false);
    port.start();                                // start激活端口,开始监听端口
    port.postMessage(2);                // 发送消息
  }
</script>
onconnect = function (event) {
    var port = event.ports[0];
    port.onmessage = function (e) {                    // 定义页面传过来的值e.data
        port.postMessage(e.data * e.data);        // 计算后返回
    };
};

这个例子中,我们仅展示依靠共享后台线程来提供服务,各页面中的数据不会互相干扰。

在多个页面中通过共享后台线程来共享数据

接下来,我们通过一个代码示例来展示如何在多个页面之间通过共享的后台线程来共享一部分数据。

在本例中,我们展示两个示例页面,这两个示例页面共享同一个后台线程,当第一个示例页面在打开即通过一个 SharedWorker 对象创建一个后台线程,页面中展示一个 input 元素、一个“提交数据”按钮,与一个“获取数据”按钮,当用户在 input 元素中输入文字后单击“提交数据”按钮时,向后台线程提交用户在 input 元素中输入的文字,当用户单击“获取数据”按钮时,在 input 元素中显示用户提交的数据。第二个示例页面在打开时通过一个 SharedWorker 对象共享第一个示例页面中所创建的后台线程。页面中所显示元素及作用与第一个示例页面中所显示的元素及作用完全相同,当用户在第一个示例页面中提交数据后,在第二个示例页面中单击“获取数据”按钮时即可将用户在第一个示例页面中提交的数据显示在 input 元素中。

<!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>Document</title>
</head>
<body onload="window_onload();">
  <input type="text" id="text" />
  <button onclick="sendData();">提交数据</button>
  <button onclick="getData();">获取数据</button>
</body>
</html>
<script>
  var worker;
  function window_onload() {
    worker = new SharedWorker('sharedworker.js');
    var div = document.getElementById('div1');
    var port = worker.port;
    port.addEventListener('message', function(event) {
      document.getElementById('text').value = event.data;
      // console.log(event.data);
    }, false);
    port.start();
  }
  function sendData() {
    worker.port.postMessage(document.getElementById('text').value);
  }
  function getData() {
    worker.port.postMessage('get');
  }
</script>
// 页面二的代码与页面一完全相同
var data;
onconnect = function (event) {
    var port = event.ports[0];
    port.onmessage = function (e) {
        if (e.data === "get") {
            port.postMessage(data);
        } else {
            data = e.data;
        }
    };
};

当在页面一中点击“提交数据”,将数据提交到后台线程,当点击“获取数据”时,将数据输出在 input 框中。
image.png
在页面二中同样点击“获取数据”时,数据也会输出在 input 框中。

to be continue…