1. 10 Web 工作线程
    1. 10.1 简介
      1. 10.1.1 范围
      2. 10.1.2 示例
        1. 10.1.2.1 后台数字计算工作线程
        2. 10.1.2.2 使用 JavaScript 模块作为工作线程
        3. 10.1.2.3 共享工作线程简介
        4. 10.1.2.4 使用共享工作线程共享状态
        5. 10.1.2.5 委托
        6. 10.1.2.6 提供库
      3. 10.1.3 教程
        1. 10.1.3.1 创建专用工作线程
        2. 10.1.3.2 与专用工作线程通信
        3. 10.1.3.3 共享工作线程
    2. 10.2 基础设施
      1. 10.2.1 全局作用域
        1. 10.2.1.1 WorkerGlobalScope 通用接口
        2. 10.2.1.2 专用工作线程和 DedicatedWorkerGlobalScope 接口
        3. 10.2.1.3 共享工作线程和 SharedWorkerGlobalScope 接口
      2. 10.2.2 事件循环
      3. 10.2.3 运行时脚本错误
      4. 10.2.4 创建工作线程
        1. 10.2.4.1 WorkerSharedWorker 上都存在的属性
        2. 10.2.4.2 专用工作线程和 Worker 接口
        3. 10.2.4.3 共享工作线程和 SharedWorker 接口
      5. 10.2.5 并发硬件功能
    3. 10.3 工作线程可用的 API
      1. 10.3.1 WorkerNavigator 接口
      2. 10.3.2 WorkerLocation 接口

10 Web 工作线程

Web_Workers_API

所有当前引擎都支持。

Firefox3.5+Safari4+Chrome2+
Opera10.6+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox Android?Safari iOS5+Chrome Android?WebView Android?Samsung Internet?Opera Android11+

Web_Workers_API/Using_web_workers

10.1 简介

10.1.1 范围

本规范定义了一个 API,用于在后台独立于任何用户界面脚本运行脚本。

这允许长时间运行的脚本不受响应点击或其他用户交互的脚本的干扰,并允许执行长时间任务而不会产生延迟以保持页面响应。

工作线程(此处将这些后台脚本称为工作线程)相对重量级,并且不适合大量使用。例如,为四百万像素图像的每个像素启动一个工作线程是不合适的。以下示例显示了一些工作线程的适当用途。

通常,工作线程预计具有较长的生命周期、较高的启动性能成本和较高的每个实例内存成本。

10.1.2 示例

工作线程可以用于各种用途。以下子部分展示了这些用途的各种示例。

10.1.2.1 后台数字计算工作线程

工作线程最简单的用途是在不中断用户界面的情况下执行计算量大的任务。

在此示例中,主文档生成一个工作线程来(简单地)计算质数,并逐步显示最近找到的质数。

主页面如下所示

<!DOCTYPE HTML>
<html lang="en">
 <head>
  <meta charset="utf-8">
  <title>Worker example: One-core computation</title>
 </head>
 <body>
  <p>The highest prime number discovered so far is: <output id="result"></output></p>
  <script>
   var worker = new Worker('worker.js');
   worker.onmessage = function (event) {
     document.getElementById('result').textContent = event.data;
   };
  </script>
 </body>
</html>

Worker() 构造函数调用创建了一个工作线程并返回一个表示该工作线程的 Worker 对象,该对象用于与工作线程通信。该对象的 onmessage 事件处理程序允许代码接收来自工作线程的消息。

工作线程本身如下所示

var n = 1;
search: while (true) {
  n += 1;
  for (var i = 2; i <= Math.sqrt(n); i += 1)
    if (n % i == 0)
     continue search;
  // found a prime!
  postMessage(n);
}

此代码的大部分内容只是一个未优化的质数搜索。当找到质数时,postMessage() 方法用于向页面发送消息。

在线查看此示例.

10.1.2.2 使用 JavaScript 模块作为工作线程

我们到目前为止的所有示例都显示了运行经典脚本的工作线程。工作线程可以使用模块脚本实例化,这具有通常的好处:能够使用 JavaScript import 语句导入其他模块;默认情况下为严格模式;以及顶级声明不会污染工作线程的全局作用域。

由于 import 语句可用,因此importScripts() 方法将在模块工作线程内部自动失败。

在此示例中,主文档使用工作线程执行脱离开发线程的图像处理。它从另一个模块导入使用的过滤器。

主页面如下所示

<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<title>Worker example: image decoding</title>

<p>
  <label>
    Type an image URL to decode
    <input type="url" id="image-url" list="image-list">
    <datalist id="image-list">
      <option value="https://html.whatwg.com.cn/images/drawImage.png">
      <option value="https://html.whatwg.com.cn/images/robots.jpeg">
      <option value="https://html.whatwg.com.cn/images/arcTo2.png">
    </datalist>
  </label>
</p>

<p>
  <label>
    Choose a filter to apply
    <select id="filter">
      <option value="none">none</option>
      <option value="grayscale">grayscale</option>
      <option value="brighten">brighten by 20%</option>
    </select>
  </label>
</p>

<div id="output"></div>

<script type="module">
  const worker = new Worker("worker.js", { type: "module" });
  worker.onmessage = receiveFromWorker;

  const url = document.querySelector("#image-url");
  const filter = document.querySelector("#filter");
  const output = document.querySelector("#output");

  url.oninput = updateImage;
  filter.oninput = sendToWorker;

  let imageData, context;

  function updateImage() {
    const img = new Image();
    img.src = url.value;

    img.onload = () => {
      const canvas = document.createElement("canvas");
      canvas.width = img.width;
      canvas.height = img.height;

      context = canvas.getContext("2d");
      context.drawImage(img, 0, 0);
      imageData = context.getImageData(0, 0, canvas.width, canvas.height);

      sendToWorker();
      output.replaceChildren(canvas);
    };
  }

  function sendToWorker() {
    worker.postMessage({ imageData, filter: filter.value });
  }

  function receiveFromWorker(e) {
    context.putImageData(e.data, 0, 0);
  }
</script>

然后工作线程文件为

import * as filters from "./filters.js";

self.onmessage = e => {
  const { imageData, filter } = e.data;
  filters[filter](imageData);
  self.postMessage(imageData, [imageData.data.buffer]);
};

它导入文件 filters.js

export function none() {}

export function grayscale({ data: d }) {
  for (let i = 0; i < d.length; i += 4) {
    const [r, g, b] = [d[i], d[i + 1], d[i + 2]];

    // CIE luminance for the RGB
    // The human eye is bad at seeing red and blue, so we de-emphasize them.
    d[i] = d[i + 1] = d[i + 2] = 0.2126 * r + 0.7152 * g + 0.0722 * b;
  }
};

export function brighten({ data: d }) {
  for (let i = 0; i < d.length; ++i) {
    d[i] *= 1.2;
  }
};

在线查看此示例.

10.1.2.3 共享工作线程简介

SharedWorker

所有当前引擎都支持。

Firefox29+Safari16+Chrome5+
Opera10.6+Edge79+
Edge(旧版)?Internet Explorer不支持
Firefox Android33+Safari iOS16+Chrome Android不支持WebView Android?Samsung Internet4.0–5.0Opera Android11–14

本节使用 Hello World 示例介绍共享工作线程。共享工作线程使用略微不同的 API,因为每个工作线程可以有多个连接。

第一个示例演示了如何连接到工作线程以及工作线程如何在连接到页面时向页面发送消息。接收到的消息显示在日志中。

这是 HTML 页面

<!DOCTYPE HTML>
<html lang="en">
<meta charset="utf-8">
<title>Shared workers: demo 1</title>
<pre id="log">Log:</pre>
<script>
  var worker = new SharedWorker('test.js');
  var log = document.getElementById('log');
  worker.port.onmessage = function(e) { // note: not worker.onmessage!
    log.textContent += '\n' + e.data;
  }
</script>

这是 JavaScript 工作线程

onconnect = function(e) {
  var port = e.ports[0];
  port.postMessage('Hello World!');
}

在线查看此示例.


第二个示例通过更改两件事扩展了第一个示例:首先,使用 addEventListener() 而不是 事件处理程序 IDL 属性 接收消息,其次,向工作线程发送消息,导致工作线程发送另一条消息作为回复。接收到的消息再次显示在日志中。

这是 HTML 页面

<!DOCTYPE HTML>
<html lang="en">
<meta charset="utf-8">
<title>Shared workers: demo 2</title>
<pre id="log">Log:</pre>
<script>
  var worker = new SharedWorker('test.js');
  var log = document.getElementById('log');
  worker.port.addEventListener('message', function(e) {
    log.textContent += '\n' + e.data;
  }, false);
  worker.port.start(); // note: need this when using addEventListener
  worker.port.postMessage('ping');
</script>

这是 JavaScript 工作线程

onconnect = function(e) {
  var port = e.ports[0];
  port.postMessage('Hello World!');
  port.onmessage = function(e) {
    port.postMessage('pong'); // not e.ports[0].postMessage!
    // e.target.postMessage('pong'); would work also
  }
}

在线查看此示例.


最后,扩展了示例以演示两个页面如何连接到同一个工作线程;在本例中,第二个页面仅仅位于第一个页面的 iframe 中,但相同的原理也适用于单独的 顶级可遍历对象 中的完全独立的页面。

这是外部 HTML 页面

<!DOCTYPE HTML>
<html lang="en">
<meta charset="utf-8">
<title>Shared workers: demo 3</title>
<pre id="log">Log:</pre>
<script>
  var worker = new SharedWorker('test.js');
  var log = document.getElementById('log');
  worker.port.addEventListener('message', function(e) {
    log.textContent += '\n' + e.data;
  }, false);
  worker.port.start();
  worker.port.postMessage('ping');
</script>
<iframe src="inner.html"></iframe>

这是内部 HTML 页面

<!DOCTYPE HTML>
<html lang="en">
<meta charset="utf-8">
<title>Shared workers: demo 3 inner frame</title>
<pre id=log>Inner log:</pre>
<script>
  var worker = new SharedWorker('test.js');
  var log = document.getElementById('log');
  worker.port.onmessage = function(e) {
   log.textContent += '\n' + e.data;
  }
</script>

这是 JavaScript 工作线程

var count = 0;
onconnect = function(e) {
  count += 1;
  var port = e.ports[0];
  port.postMessage('Hello World! You are connection #' + count);
  port.onmessage = function(e) {
    port.postMessage('pong');
  }
}

在线查看此示例.

10.1.2.4 使用共享工作线程共享状态

在此示例中,可以打开多个窗口(查看器),这些窗口都查看同一张地图。所有窗口共享相同的地图信息,单个工作线程协调所有查看器。每个查看器可以独立移动,但如果它们在地图上设置任何数据,则所有查看器都会更新。

主页面没什么意思,它只是提供了一种打开查看器的方法

<!DOCTYPE HTML>
<html lang="en">
 <head>
  <meta charset="utf-8">
  <title>Workers example: Multiviewer</title>
  <script>
   function openViewer() {
     window.open('viewer.html');
   }
  </script>
 </head>
 <body>
  <p><button type=button onclick="openViewer()">Open a new
  viewer</button></p>
  <p>Each viewer opens in a new window. You can have as many viewers
  as you like, they all view the same data.</p>
 </body>
</html>

查看器更复杂

<!DOCTYPE HTML>
<html lang="en">
 <head>
  <meta charset="utf-8">
  <title>Workers example: Multiviewer viewer</title>
  <script>
   var worker = new SharedWorker('worker.js', 'core');

   // CONFIGURATION
   function configure(event) {
     if (event.data.substr(0, 4) != 'cfg ') return;
     var name = event.data.substr(4).split(' ', 1)[0];
     // update display to mention our name is name
     document.getElementsByTagName('h1')[0].textContent += ' ' + name;
     // no longer need this listener
     worker.port.removeEventListener('message', configure, false);
   }
   worker.port.addEventListener('message', configure, false);

   // MAP
   function paintMap(event) {
     if (event.data.substr(0, 4) != 'map ') return;
     var data = event.data.substr(4).split(',');
     // display tiles data[0] .. data[8]
     var canvas = document.getElementById('map');
     var context = canvas.getContext('2d');
     for (var y = 0; y < 3; y += 1) {
       for (var x = 0; x < 3; x += 1) {
         var tile = data[y * 3 + x];
         if (tile == '0')
           context.fillStyle = 'green';
         else
           context.fillStyle = 'maroon';
         context.fillRect(x * 50, y * 50, 50, 50);
       }
     }
   }
   worker.port.addEventListener('message', paintMap, false);

   // PUBLIC CHAT
   function updatePublicChat(event) {
     if (event.data.substr(0, 4) != 'txt ') return;
     var name = event.data.substr(4).split(' ', 1)[0];
     var message = event.data.substr(4 + name.length + 1);
     // display "<name> message" in public chat
     var public = document.getElementById('public');
     var p = document.createElement('p');
     var n = document.createElement('button');
     n.textContent = '<' + name + '> ';
     n.onclick = function () { worker.port.postMessage('msg ' + name); };
     p.appendChild(n);
     var m = document.createElement('span');
     m.textContent = message;
     p.appendChild(m);
     public.appendChild(p);
   }
   worker.port.addEventListener('message', updatePublicChat, false);

   // PRIVATE CHAT
   function startPrivateChat(event) {
     if (event.data.substr(0, 4) != 'msg ') return;
     var name = event.data.substr(4).split(' ', 1)[0];
     var port = event.ports[0];
     // display a private chat UI
     var ul = document.getElementById('private');
     var li = document.createElement('li');
     var h3 = document.createElement('h3');
     h3.textContent = 'Private chat with ' + name;
     li.appendChild(h3);
     var div = document.createElement('div');
     var addMessage = function(name, message) {
       var p = document.createElement('p');
       var n = document.createElement('strong');
       n.textContent = '<' + name + '> ';
       p.appendChild(n);
       var t = document.createElement('span');
       t.textContent = message;
       p.appendChild(t);
       div.appendChild(p);
     };
     port.onmessage = function (event) {
       addMessage(name, event.data);
     };
     li.appendChild(div);
     var form = document.createElement('form');
     var p = document.createElement('p');
     var input = document.createElement('input');
     input.size = 50;
     p.appendChild(input);
     p.appendChild(document.createTextNode(' '));
     var button = document.createElement('button');
     button.textContent = 'Post';
     p.appendChild(button);
     form.onsubmit = function () {
       port.postMessage(input.value);
       addMessage('me', input.value);
       input.value = '';
       return false;
     };
     form.appendChild(p);
     li.appendChild(form);
     ul.appendChild(li);
   }
   worker.port.addEventListener('message', startPrivateChat, false);

   worker.port.start();
  </script>
 </head>
 <body>
  <h1>Viewer</h1>
  <h2>Map</h2>
  <p><canvas id="map" height=150 width=150></canvas></p>
  <p>
   <button type=button onclick="worker.port.postMessage('mov left')">Left</button>
   <button type=button onclick="worker.port.postMessage('mov up')">Up</button>
   <button type=button onclick="worker.port.postMessage('mov down')">Down</button>
   <button type=button onclick="worker.port.postMessage('mov right')">Right</button>
   <button type=button onclick="worker.port.postMessage('set 0')">Set 0</button>
   <button type=button onclick="worker.port.postMessage('set 1')">Set 1</button>
  </p>
  <h2>Public Chat</h2>
  <div id="public"></div>
  <form onsubmit="worker.port.postMessage('txt ' + message.value); message.value = ''; return false;">
   <p>
    <input type="text" name="message" size="50">
    <button>Post</button>
   </p>
  </form>
  <h2>Private Chat</h2>
  <ul id="private"></ul>
 </body>
</html>

查看器编写方式有一些关键要点需要注意。

多个侦听器。代码在此处附加多个事件侦听器,而不是单个消息处理函数,每个侦听器都会快速检查它是否与消息相关。在此示例中,这没有多大区别,但如果多个作者想要使用单个端口与工作线程通信进行协作,它将允许使用独立的代码,而不是必须对单个事件处理函数进行所有更改。

以这种方式注册事件侦听器还允许您在完成使用后注销特定侦听器,如本示例中的 configure() 方法所示。

最后,工作线程

var nextName = 0;
function getNextName() {
  // this could use more friendly names
  // but for now just return a number
  return nextName++;
}

var map = [
 [0, 0, 0, 0, 0, 0, 0],
 [1, 1, 0, 1, 0, 1, 1],
 [0, 1, 0, 1, 0, 0, 0],
 [0, 1, 0, 1, 0, 1, 1],
 [0, 0, 0, 1, 0, 0, 0],
 [1, 0, 0, 1, 1, 1, 1],
 [1, 1, 0, 1, 1, 0, 1],
];

function wrapX(x) {
  if (x < 0) return wrapX(x + map[0].length);
  if (x >= map[0].length) return wrapX(x - map[0].length);
  return x;
}

function wrapY(y) {
  if (y < 0) return wrapY(y + map.length);
  if (y >= map[0].length) return wrapY(y - map.length);
  return y;
}

function wrap(val, min, max) {
  if (val < min)
    return val + (max-min)+1;
  if (val > max)
    return val - (max-min)-1;
  return val;
}

function sendMapData(viewer) {
  var data = '';
  for (var y = viewer.y-1; y <= viewer.y+1; y += 1) {
    for (var x = viewer.x-1; x <= viewer.x+1; x += 1) {
      if (data != '')
        data += ',';
      data += map[wrap(y, 0, map[0].length-1)][wrap(x, 0, map.length-1)];
    }
  }
  viewer.port.postMessage('map ' + data);
}

var viewers = {};
onconnect = function (event) {
  var name = getNextName();
  event.ports[0]._data = { port: event.ports[0], name: name, x: 0, y: 0, };
  viewers[name] = event.ports[0]._data;
  event.ports[0].postMessage('cfg ' + name);
  event.ports[0].onmessage = getMessage;
  sendMapData(event.ports[0]._data);
};

function getMessage(event) {
  switch (event.data.substr(0, 4)) {
    case 'mov ':
      var direction = event.data.substr(4);
      var dx = 0;
      var dy = 0;
      switch (direction) {
        case 'up': dy = -1; break;
        case 'down': dy = 1; break;
        case 'left': dx = -1; break;
        case 'right': dx = 1; break;
      }
      event.target._data.x = wrapX(event.target._data.x + dx);
      event.target._data.y = wrapY(event.target._data.y + dy);
      sendMapData(event.target._data);
      break;
    case 'set ':
      var value = event.data.substr(4);
      map[event.target._data.y][event.target._data.x] = value;
      for (var viewer in viewers)
        sendMapData(viewers[viewer]);
      break;
    case 'txt ':
      var name = event.target._data.name;
      var message = event.data.substr(4);
      for (var viewer in viewers)
        viewers[viewer].port.postMessage('txt ' + name + ' ' + message);
      break;
    case 'msg ':
      var party1 = event.target._data;
      var party2 = viewers[event.data.substr(4).split(' ', 1)[0]];
      if (party2) {
        var channel = new MessageChannel();
        party1.port.postMessage('msg ' + party2.name, [channel.port1]);
        party2.port.postMessage('msg ' + party1.name, [channel.port2]);
      }
      break;
  }
}

连接到多个页面。脚本使用 onconnect 事件侦听器来侦听多个连接。

直接通道。当工作线程从一个查看器接收“msg”消息并命名另一个查看器时,它会在两者之间建立直接连接,以便两个查看器可以直接通信,而无需工作线程代理所有消息。

在线查看此示例.

10.1.2.5 委托

随着多核 CPU 变得越来越普遍,获得更好性能的一种方法是在多个工作线程之间拆分计算量大的任务。在此示例中,要为从 1 到 10,000,000 的每个数字执行的计算量大的任务被分配给十个子工作线程。

主页面如下所示,它只报告结果

<!DOCTYPE HTML>
<html lang="en">
 <head>
  <meta charset="utf-8">
  <title>Worker example: Multicore computation</title>
 </head>
 <body>
  <p>Result: <output id="result"></output></p>
  <script>
   var worker = new Worker('worker.js');
   worker.onmessage = function (event) {
     document.getElementById('result').textContent = event.data;
   };
  </script>
 </body>
</html>

工作线程本身如下所示

// settings
var num_workers = 10;
var items_per_worker = 1000000;

// start the workers
var result = 0;
var pending_workers = num_workers;
for (var i = 0; i < num_workers; i += 1) {
  var worker = new Worker('core.js');
  worker.postMessage(i * items_per_worker);
  worker.postMessage((i+1) * items_per_worker);
  worker.onmessage = storeResult;
}

// handle the results
function storeResult(event) {
  result += 1*event.data;
  pending_workers -= 1;
  if (pending_workers <= 0)
    postMessage(result); // finished!
}

它包含一个循环来启动子工作线程,然后是一个处理程序,等待所有子工作线程响应。

子工作线程的实现如下

var start;
onmessage = getStart;
function getStart(event) {
  start = 1*event.data;
  onmessage = getEnd;
}

var end;
function getEnd(event) {
  end = 1*event.data;
  onmessage = null;
  work();
}

function work() {
  var result = 0;
  for (var i = start; i < end; i += 1) {
    // perform some complex calculation here
    result += 1;
  }
  postMessage(result);
  close();
}

它们在两个事件中接收两个数字,对由此指定范围内的数字执行计算,然后将结果报告回父级。

在线查看此示例.

10.1.2.6 提供库

假设提供了三个任务的加密库

生成公钥/私钥对
获取一个端口,它将在该端口上发送两条消息,第一条是公钥,第二条是私钥。
给定明文和公钥,返回相应的密文
获取一个端口,可以向该端口发送任意数量的消息,第一条消息提供公钥,其余消息提供明文,每条消息都加密然后在同一通道上作为密文发送。用户可以在完成内容加密后关闭端口。
给定密文和私钥,返回相应的明文
获取一个端口,可以向该端口发送任意数量的消息,第一条消息提供私钥,其余消息提供密文,每条消息都解密然后在同一通道上作为明文发送。用户可以在完成内容解密后关闭端口。

库本身如下所示

function handleMessage(e) {
  if (e.data == "genkeys")
    genkeys(e.ports[0]);
  else if (e.data == "encrypt")
    encrypt(e.ports[0]);
  else if (e.data == "decrypt")
    decrypt(e.ports[0]);
}

function genkeys(p) {
  var keys = _generateKeyPair();
  p.postMessage(keys[0]);
  p.postMessage(keys[1]);
}

function encrypt(p) {
  var key, state = 0;
  p.onmessage = function (e) {
    if (state == 0) {
      key = e.data;
      state = 1;
    } else {
      p.postMessage(_encrypt(key, e.data));
    }
  };
}

function decrypt(p) {
  var key, state = 0;
  p.onmessage = function (e) {
    if (state == 0) {
      key = e.data;
      state = 1;
    } else {
      p.postMessage(_decrypt(key, e.data));
    }
  };
}

// support being used as a shared worker as well as a dedicated worker
if ('onmessage' in this) // dedicated worker
  onmessage = handleMessage;
else // shared worker
  onconnect = function (e) { e.port.onmessage = handleMessage; }


// the "crypto" functions:

function _generateKeyPair() {
  return [Math.random(), Math.random()];
}

function _encrypt(k, s) {
  return 'encrypted-' + k + ' ' + s;
}

function _decrypt(k, s) {
  return s.substr(s.indexOf(' ')+1);
}

请注意,此处的加密函数只是存根,不执行真正的加密。

此库可以使用以下方法

<!DOCTYPE HTML>
<html lang="en">
 <head>
  <meta charset="utf-8">
  <title>Worker example: Crypto library</title>
  <script>
   const cryptoLib = new Worker('libcrypto-v1.js'); // or could use 'libcrypto-v2.js'
   function startConversation(source, message) {
     const messageChannel = new MessageChannel();
     source.postMessage(message, [messageChannel.port2]);
     return messageChannel.port1;
   }
   function getKeys() {
     let state = 0;
     startConversation(cryptoLib, "genkeys").onmessage = function (e) {
       if (state === 0)
         document.getElementById('public').value = e.data;
       else if (state === 1)
         document.getElementById('private').value = e.data;
       state += 1;
     };
   }
   function enc() {
     const port = startConversation(cryptoLib, "encrypt");
     port.postMessage(document.getElementById('public').value);
     port.postMessage(document.getElementById('input').value);
     port.onmessage = function (e) {
       document.getElementById('input').value = e.data;
       port.close();
     };
   }
   function dec() {
     const port = startConversation(cryptoLib, "decrypt");
     port.postMessage(document.getElementById('private').value);
     port.postMessage(document.getElementById('input').value);
     port.onmessage = function (e) {
       document.getElementById('input').value = e.data;
       port.close();
     };
   }
  </script>
  <style>
   textarea { display: block; }
  </style>
 </head>
 <body onload="getKeys()">
  <fieldset>
   <legend>Keys</legend>
   <p><label>Public Key: <textarea id="public"></textarea></label></p>
   <p><label>Private Key: <textarea id="private"></textarea></label></p>
  </fieldset>
  <p><label>Input: <textarea id="input"></textarea></label></p>
  <p><button onclick="enc()">Encrypt</button> <button onclick="dec()">Decrypt</button></p>
 </body>
</html>

但是,API 的后续版本可能希望将所有加密工作卸载到子工作线程上。这可以通过以下方式完成

function handleMessage(e) {
  if (e.data == "genkeys")
    genkeys(e.ports[0]);
  else if (e.data == "encrypt")
    encrypt(e.ports[0]);
  else if (e.data == "decrypt")
    decrypt(e.ports[0]);
}

function genkeys(p) {
  var generator = new Worker('libcrypto-v2-generator.js');
  generator.postMessage('', [p]);
}

function encrypt(p) {
  p.onmessage = function (e) {
    var key = e.data;
    var encryptor = new Worker('libcrypto-v2-encryptor.js');
    encryptor.postMessage(key, [p]);
  };
}

function encrypt(p) {
  p.onmessage = function (e) {
    var key = e.data;
    var decryptor = new Worker('libcrypto-v2-decryptor.js');
    decryptor.postMessage(key, [p]);
  };
}

// support being used as a shared worker as well as a dedicated worker
if ('onmessage' in this) // dedicated worker
  onmessage = handleMessage;
else // shared worker
  onconnect = function (e) { e.ports[0].onmessage = handleMessage };

然后,小型子工作线程将如下所示。

用于生成密钥对

onmessage = function (e) {
  var k = _generateKeyPair();
  e.ports[0].postMessage(k[0]);
  e.ports[0].postMessage(k[1]);
  close();
}

function _generateKeyPair() {
  return [Math.random(), Math.random()];
}

用于加密

onmessage = function (e) {
  var key = e.data;
  e.ports[0].onmessage = function (e) {
    var s = e.data;
    postMessage(_encrypt(key, s));
  }
}

function _encrypt(k, s) {
  return 'encrypted-' + k + ' ' + s;
}

用于解密

onmessage = function (e) {
  var key = e.data;
  e.ports[0].onmessage = function (e) {
    var s = e.data;
    postMessage(_decrypt(key, s));
  }
}

function _decrypt(k, s) {
  return s.substr(s.indexOf(' ')+1);
}

请注意,API 的用户甚至不必知道发生了这种情况——API 没有改变;库可以在不更改其 API 的情况下委托给子工作线程,即使它正在使用消息通道接受数据。

在线查看此示例.

10.1.3 教程

10.1.3.1 创建专用工作线程

创建工作线程需要一个指向 JavaScript 文件的 URL。Worker() 构造函数只使用指向该文件的 URL 作为其参数调用;然后创建工作线程并返回

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

如果您希望您的工作线程脚本被解释为模块脚本而不是默认的经典脚本,则需要使用稍微不同的签名。

var worker = new Worker('helper.mjs', { type: "module" });
10.1.3.2 与专用工作线程通信

专用工作线程在后台使用MessagePort对象,因此支持所有相同的功能,例如发送结构化数据、传输二进制数据和传输其他端口。

要接收来自专用工作线程的消息,请在Worker对象上使用onmessage 事件处理程序 IDL 属性

worker.onmessage = function (event) { ... };

您还可以使用addEventListener()方法。

专用工作线程使用的隐式MessagePort在其创建时会隐式启用其端口消息队列,因此在Worker接口上没有等效于MessagePort接口的start()方法。

发送数据到工作线程,请使用postMessage()方法。结构化数据可以通过此通信通道发送。要有效地发送ArrayBuffer对象(通过传输而不是克隆它们),请将它们列在第二个参数的数组中。

worker.postMessage({
  operation: 'find-edges',
  input: buffer, // an ArrayBuffer object
  threshold: 0.6,
}, [buffer]);

要在工作线程内部接收消息,使用onmessage 事件处理程序 IDL 属性

onmessage = function (event) { ... };

您还可以再次使用addEventListener()方法。

在这两种情况下,数据都提供在事件对象的data属性中。

要发送回复消息,您再次使用postMessage()。它以相同的方式支持结构化数据。

postMessage(event.data.input, [event.data.input]); // transfer the buffer back
10.1.3.3 共享工作线程

SharedWorker

所有当前引擎都支持。

Firefox29+Safari16+Chrome5+
Opera10.6+Edge79+
Edge(旧版)?Internet Explorer不支持
Firefox Android33+Safari iOS16+Chrome Android不支持WebView Android?Samsung Internet4.0–5.0Opera Android11–14

共享工作线程由用于创建它的脚本的 URL 标识,可以选择使用显式名称。该名称允许启动特定共享工作线程的多个实例。

共享工作线程按来源限定范围。使用相同名称的两个不同站点不会发生冲突。但是,如果一个页面尝试使用与同一站点上的另一个页面相同的共享工作线程名称,但使用不同的脚本 URL,则会失败。

创建共享工作线程是使用SharedWorker()构造函数完成的。此构造函数将要使用的脚本的 URL 作为其第一个参数,以及工作线程的名称(如果有)作为第二个参数。

var worker = new SharedWorker('service.js');

与共享工作线程的通信是通过显式的MessagePort对象完成的。由SharedWorker()构造函数返回的对象在其port属性中保存对端口的引用。

worker.port.onmessage = function (event) { ... };
worker.port.postMessage('some message');
worker.port.postMessage({ foo: 'structured', bar: ['data', 'also', 'possible']});

在共享工作线程内部,使用connect事件宣布工作线程的新客户端。新客户端的端口由事件对象的source属性给出。

onconnect = function (event) {
  var newPort = event.source;
  // set up a listener
  newPort.onmessage = function (event) { ... };
  // send a message back to the port
  newPort.postMessage('ready!'); // can also send structured data, of course
};

10.2 基础设施

本标准定义了两种工作线程:专用工作线程和共享工作线程。专用工作线程一旦创建,就会与其创建者链接,但可以使用消息端口从专用工作线程与多个其他浏览上下文或工作线程通信。另一方面,共享工作线程是命名的,一旦创建,在相同来源中运行的任何脚本都可以获取对该工作线程的引用并与其通信。服务工作线程定义了第三种类型。[SW]

10.2.1 全局作用域

全局作用域是工作线程的“内部”。

10.2.1.1 WorkerGlobalScope通用接口

WorkerGlobalScope

所有当前引擎都支持。

Firefox3.5+Safari4+Chrome4+
Opera10.6+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox Android?Safari iOS5+Chrome Android?WebView Android?Samsung Internet?Opera Android11+

WorkerGlobalScope作为特定类型的worker全局作用域对象的基类,包括DedicatedWorkerGlobalScopeSharedWorkerGlobalScopeServiceWorkerGlobalScope

workerGlobal.self
返回workerGlobal
workerGlobal.location
返回workerGlobalWorkerLocation对象。
workerGlobal.navigator
返回workerGlobalWorkerNavigator对象。
workerGlobal.importScripts(...urls)
获取urls中的每个URL,按传递的顺序逐一执行它们,然后返回(如果出现问题则抛出错误)。

以下是事件处理程序(及其对应的事件处理程序事件类型),作为事件处理程序 IDL 属性,由实现WorkerGlobalScope接口的对象支持。

事件处理程序 事件处理程序事件类型
onerror

WorkerGlobalScope/error_event

所有当前引擎都支持。

Firefox3.5+Safari4+Chrome4+
Opera11.5+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox Android?Safari iOS5+Chrome Android?WebView Android?Samsung Internet?Opera Android?
error
onlanguagechange

WorkerGlobalScope/languagechange_event

所有当前引擎都支持。

Firefox74+Safari4+Chrome4+
Opera11.5+Edge79+
Edge(旧版)?Internet Explorer不支持
Firefox Android?Safari iOS5+Chrome Android?WebView Android37+Samsung Internet?Opera Android?
languagechange
onoffline

WorkerGlobalScope/offline_event

Firefox29+Safari8+Chrome
Opera?Edge
Edge(旧版)?Internet Explorer不支持
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?
offline
ononline

WorkerGlobalScope/online_event

Firefox29+Safari8+Chrome
Opera?Edge
Edge(旧版)?Internet Explorer不支持
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?
online
onrejectionhandled rejectionhandled
onunhandledrejection unhandledrejection
10.2.1.2 专用工作线程和DedicatedWorkerGlobalScope接口

DedicatedWorkerGlobalScope

所有当前引擎都支持。

Firefox3.5+Safari4+Chrome4+
Opera10.6+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox Android?Safari iOS5+Chrome Android?WebView Android?Samsung Internet?Opera Android11+

DedicatedWorkerGlobalScope对象的行为就像它们有一个与之关联的隐式MessagePort一样。此端口是创建工作线程时设置的通道的一部分,但它不会公开。

dedicatedWorkerGlobal.name

返回dedicatedWorkerGlobal名称,即提供给Worker构造函数的值。主要用于调试。

dedicatedWorkerGlobal.postMessage(message [, transfer ])
dedicatedWorkerGlobal.postMessage(message [, { transfer } ])

克隆message并将其传输到与dedicatedWorkerGlobal关联的Worker对象。transfer可以作为要传输而不是克隆的对象列表传递。

dedicatedWorkerGlobal.close()

中止dedicatedWorkerGlobal

以下是事件处理程序(及其对应的事件处理程序事件类型),作为事件处理程序 IDL 属性,由实现DedicatedWorkerGlobalScope接口的对象支持。

事件处理程序 事件处理程序事件类型
onmessage

DedicatedWorkerGlobalScope/message_event

所有当前引擎都支持。

Firefox3.5+Safari4+Chrome4+
Opera10.6+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox Android?Safari iOS5+Chrome Android?WebView Android37+Samsung Internet?Opera Android11.5+
message
onmessageerror

DedicatedWorkerGlobalScope/messageerror_event

所有当前引擎都支持。

Firefox57+Safari16.4+Chrome60+
Opera?Edge79+
Edge (Legacy)18Internet Explorer
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android47+
messageerror
10.2.1.3 共享工作线程和SharedWorkerGlobalScope接口

SharedWorkerGlobalScope

所有当前引擎都支持。

Firefox29+Safari16+Chrome4+
Opera10.6+Edge79+
Edge(旧版)?Internet Explorer不支持
Firefox Android?Safari iOS16+Chrome Android?WebView Android37+Samsung Internet?Opera Android11+

共享工作线程通过其SharedWorkerGlobalScope对象上的每个连接的connect事件接收消息端口。

sharedWorkerGlobalname

返回 sharedWorkerGlobalname,即传递给 SharedWorker 构造函数的值。多个 SharedWorker 对象可以通过重用相同的名称对应于同一个共享工作线程(以及 SharedWorkerGlobalScope)。

sharedWorkerGlobalclose()

中止 sharedWorkerGlobal

以下是作为 事件处理程序 IDL 属性,由实现 SharedWorkerGlobalScope 接口的对象支持的 事件处理程序(及其对应的 事件处理程序事件类型)。

事件处理程序 事件处理程序事件类型
onconnect

SharedWorkerGlobalScope/connect_event

所有当前引擎都支持。

Firefox29+Safari16+Chrome4+
Opera10.6+Edge79+
Edge(旧版)?Internet Explorer不支持
Firefox Android?Safari iOS16+Chrome Android?WebView Android37+Samsung Internet?Opera Android11+
connect

10.2.2 事件循环

一个 工作线程事件循环任务队列仅包含事件、回调和网络活动作为 任务

每个 WorkerGlobalScope 对象都有一个 closing 标志,最初为 false,但在 请求工作线程关闭时可以设置为 true。

一旦 WorkerGlobalScopeclosing 标志设置为 true,事件循环任务队列将丢弃任何进一步添加到其中的 任务(队列中已有的任务不受影响,除非另有说明)。实际上,一旦 closing 标志为 true,计时器将停止触发,所有挂起的后台操作的通知都将被丢弃,等等。

10.2.3 运行时脚本错误

每当工作线程的某个脚本中发生未捕获的运行时脚本错误时,如果该错误不是在处理先前的脚本错误期间发生的,则用户代理将为工作线程的 WorkerGlobalScope 对象 报告该错误。

10.2.4 创建工作线程

10.2.4.1 WorkerSharedWorker 上都存在的属性

以下是作为 事件处理程序 IDL 属性,由 WorkerSharedWorker 对象支持的 事件处理程序(及其对应的 事件处理程序事件类型)。

事件处理程序 事件处理程序事件类型
onerror

ServiceWorker/error_event

所有当前引擎都支持。

Firefox44+Safari11.1+Chrome40+
Opera?Edge79+
Edge (旧版)17+Internet Explorer不支持
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?

SharedWorker/error_event

所有当前引擎都支持。

Firefox29+Safari16+Chrome5+
Opera10.6+Edge79+
Edge(旧版)?Internet Explorer不支持
Firefox Android33+Safari iOS16+Chrome Android不支持WebView Android?Samsung Internet4.0–5.0Opera Android11–14

Worker/error_event

所有当前引擎都支持。

Firefox3.5+Safari4+Chrome4+
Opera10.6+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox Android?Safari iOS5+Chrome Android?WebView Android?Samsung Internet?Opera Android11+
error
10.2.4.2 专用工作线程和 Worker 接口

Worker

所有当前引擎都支持。

Firefox3.5+Safari4+Chrome2+
Opera10.6+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox Android?Safari iOS5+Chrome Android?WebView Android?Samsung Internet?Opera Android11+
worker = new Worker(scriptURL [, options ])

返回一个新的 Worker 对象。scriptURL 将在后台获取并执行,创建一个新的全局环境,worker 代表该环境的通信通道。options 可用于通过 name 选项定义该全局环境的 name,主要用于调试目的。它还可以确保此新的全局环境支持 JavaScript 模块(指定 type: "module"),如果指定了该选项,还可以用于通过 credentials 选项指定如何获取 scriptURL

workerterminate()
中止 worker 关联的全局环境。
workerpostMessage(message [, transfer ])
workerpostMessage(message [, { transfer } ])

克隆 message 并将其传输到 worker 的全局环境。transfer 可以作为要传输而不是克隆的对象列表传递。

postMessage() 方法的第一个参数可以是结构化数据。

worker.postMessage({opcode: 'activate', device: 1938, parameters: [23, 102]});

以下是作为 事件处理程序 IDL 属性,由实现 Worker 接口的对象支持的 事件处理程序(及其对应的 事件处理程序事件类型)。

事件处理程序 事件处理程序事件类型
onmessage message
onmessageerror messageerror
10.2.4.3 共享工作线程和 SharedWorker 接口

SharedWorker

所有当前引擎都支持。

Firefox29+Safari16+Chrome5+
Opera10.6+Edge79+
Edge(旧版)?Internet Explorer不支持
Firefox Android33+Safari iOS16+Chrome Android不支持WebView Android?Samsung Internet4.0–5.0Opera Android11–14
sharedWorker = new SharedWorker(scriptURL [, name ])

返回一个新的 SharedWorker 对象。scriptURL 将在后台获取并执行,创建一个新的全局环境,sharedWorker 代表该环境的通信通道。name 可用于定义该全局环境的 name

sharedWorker = new SharedWorker(scriptURL [, options ])

返回一个新的 SharedWorker 对象。scriptURL 将在后台获取并执行,创建一个新的全局环境,sharedWorker 代表该环境的通信通道。options 可用于通过 name 选项定义该全局环境的 name。它还可以确保此新的全局环境支持 JavaScript 模块(指定 type: "module"),如果指定了该选项,还可以用于通过 credentials 选项指定如何获取 scriptURL。请注意,尝试使用 options(其 typecredentials 值与现有共享工作线程不匹配)构造共享工作线程将导致返回的 sharedWorker 触发错误事件,并且不会连接到现有的共享工作线程。

sharedWorkerport

返回 sharedWorkerMessagePort 对象,该对象可用于与全局环境通信。

selfnavigatorhardwareConcurrency

返回用户代理可能可用的逻辑处理器数量。

10.3 可用于工作线程的 API

10.3.1 WorkerNavigator 接口

WorkerNavigator

所有当前引擎都支持。

Firefox3.5+Safari4+Chrome4+
Opera12.1+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox AndroidSafari iOS5+Chrome AndroidWebView AndroidSamsung InternetOpera Android12.1+

WorkerNavigator 接口实现了 Navigator 接口的一个子集,包括以下 API

10.3.2 WorkerLocation 接口

WorkerLocation/toString

所有当前引擎都支持。

Firefox3.5+Safari4+Chrome4+
Opera15+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox AndroidSafari iOS5+Chrome AndroidWebView Android37+Samsung InternetOpera Android14+

WorkerLocation

所有当前引擎都支持。

Firefox3.5+Safari4+Chrome4+
Opera12.1+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox AndroidSafari iOS5+Chrome AndroidWebView AndroidSamsung InternetOpera Android12.1+

WorkerLocation 接口类似于 Location 接口,但缺少 assign()replace()reload()ancestorOrigins 成员。