JavaScript-workers

Workers能让你在不同线程中运行某些任务的能力,因此你可以启动任务,然后继续其他的处理。

对于多线程代码,你永远不知道你的线程什么时候将会被挂起,其他线程将会得到运行的机会。因此,如果两个线程都可以访问相同的变量,那么变量就又可能在任何时候发生意外的变化,这将导致难以发现的BUG。

为了避免Web中的这些问题,你的主代码和你的worker代码永远不能访问彼此的变量。Workers和主代码运行在完全分离的环境中,只有通过相互发送消息来进行交互。特别是,这意味着workers不能访问DOM。

有三种不同类型的 workers:

  • dedicated workers
  • shared workers
  • service workers

使用Web workers

同步的质数生成器

function generatePrimes(quota) {

  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
          return false;
       }
    }
    return true;
  }

  const primes = [];
  const maximum = 1000000;

  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() * (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }

  return primes;
}

document.querySelector('#generate').addEventListener('click', () => {
  const quota = document.querySelector('#quota').value;
  const primes = generatePrimes(quota);
  document.querySelector('#output').textContent = `Finished generating ${quota} primes!`;
});

document.querySelector('#reload').addEventListener('click', () => {
  document.querySelector('#user-input').value = 'Try typing in here immediately after pressing "Generate primes"';
  document.location.reload();
});

在这个程序中,在我们调用 generatePrimes() 之后,程序变得完全没有响应。

用worker进行质数生成

在这个例子中,首先在 https://github.com/mdn/learning-area/blob/main/javascript/asynchronous/workers/start 将文件拷贝到本地。在这个目录下有四个文件:

  • index.html
  • style.css
  • main.js
  • generate.js

“index.html” 文件和 “style.css” 文件已完成:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <script type="text/javascript" src="main.js" defer></script>
    <link href="style.css"rel="stylesheet">
  </head>

  <body>

    <label for="quota">Number of primes:</label>
    <input type="text" id="quota" name="quota" value="1000000">

    <button id="generate">Generate primes</button>
    <button id="reload">Reload</button>

    <textarea id="user-input" rows="5" cols="62">Try typing in here immediately after pressing "Generate primes"</textarea>

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

  </body>
</html>
textarea {
  display: block;
  margin: 1rem 0;
}

首先,我们可以看到 worker 代码被保存在一个与主代码隔离的脚本中。我们还可以看到,在上面的 “index.html” 中,只有主代码被包含在 <script> 标签中。

现在将下面的代码拷贝到 “main.js”中:

// 在 "generate.js" 中创建一个新的 worker
const worker = new Worker('./generate.js');

// 当用户点击 "Generate primes" 时,给 worker 发送一条消息。
// 消息中的 command 属性是 "generate", 还包含另外一个属性 "quota",即要生成的质数。
document.querySelector('#generate').addEventListener('click', () => {
  const quota = document.querySelector('#quota').value;
  worker.postMessage({
    command: 'generate',
    quota: quota
  });
});

// 当 worker 给主线程回发一条消息时,为用户更新 output 框,包含生成的质数(从 message 中获取)。
worker.addEventListener('message', message => {
  document.querySelector('#output').textContent = `Finished generating ${message.data} primes!`;
});

document.querySelector('#reload').addEventListener('click', () => {
  document.querySelector('#user-input').value = 'Try typing in here immediately after pressing "Generate primes"';
  document.location.reload();
});
  • 首先,我们使用 Worker() 构造函数创建 worker。我们传递一个指向 worker 脚本的 URL。只要 worker 被创建了,woker 脚本就会执行。

  • 其次,与同步版本一样,我们向 “Generate primes” 按钮添加一个

    click

    事件处理器。但是现在,我们不再调用

    generatePrimes()

    函数,而是使用

    worker.postMessage()

    向 worker 发送一条消息。这条消息可以携带一个参数,在本示例中我们传递一个包含两个属性的 JSON 对象:

    • command:一个用于标识我们希望 worker 所做事情的字符串(以防我们的 worker 可以做多个事情)。
    • quota:要生成的质数的数量。
  • 然后,我们向 worker 添加一个 message 消息处理器。这样 worker 就能告诉我们它是什么时候完成的,并且传递给我们任何结果数据。我们的处理器从消息的 data 属性获取数据,然后将其写入 output 元素(数据与 quota 是完全相同的,这虽然没有意义,但是这展示了其中原理)。

  • 最后,我们为 “Reload” 按钮实现了 click 事件处理器。这与同步版本完全相同。

现在到 worker 代码了。拷贝下面的代码到 “generate.js” 中:

// 监听主线程中的消息。
// 如果消息中的 command 是 "generate",则调用 `generatePrimse()`
addEventListener("message", message => {
  if (message.data.command === 'generate') {
    generatePrimes(message.data.quota);
  }
});

// 生成质数 (非常低效)
function generatePrimes(quota) {

  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
          return false;
       }
    }
    return true;
  }

  const primes = [];
  const maximum = 1000000;

  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() * (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }

  // 完成后给主线程发送一条包含我们生成的质数数量的消息消息。
  postMessage(primes.length);
}

请记住,只要主脚本创建 worker,这些代码就会运行。

worker 要做的第一件事情就是开始监听来自主脚本的消息。这通过使用 addEventListener() 实现,它在 worker 中是一个全局函数。在 message 事件处理器内部,事件的 data 属性包含一个来自主脚本的参数的副本。如果主脚本传递 generate 命令,我们就调用 generatePrimes(),传入来自消息事件的 quota 值。

generatePrimes() 函数与同步版本类似,只不过我们在完成后向主脚本发送一条消息,而不是返回一个值。我们对此使用 postMessage() (en-US) 函数,就像在 worker 中 addEventListener是全局函数一样。如我们所见,主脚本正在监听这条消息并且将会在收到消息后更新 DOM。

还有其他类型的 worker:

  • SharedWorker 可以由运行在不同窗口中的多个不同脚本共享。
  • Service worker 的行为就像代理服务器,缓存资源以便于 web 应用程序可以在用户离线时工作。他们是渐进式 Web 应用的关键组件。