介紹 Worker

在“非同步 JavaScript”模組的最後一篇文章中,我們將介紹 worker,它使你能夠在單獨的執行緒中執行某些任務。

預備知識 對本模組前面課程中介紹的 JavaScript 基礎知識和非同步概念有紮實的理解。
學習成果
  • 如何使用專用 Web Worker,以及原因。
  • 瞭解其他型別 Web Worker 的用途,例如共享 Worker 和服務 Worker。

在本模組的第一篇文章中,我們看到了當程式中有一個長時間執行的同步任務時會發生什麼——整個視窗變得完全無響應。從根本上說,這是因為程式是 單執行緒 的。 執行緒 是程式遵循的一系列指令。由於程式由單個執行緒組成,它一次只能做一件事:因此,如果它正在等待我們長時間執行的同步呼叫返回,它就不能做任何其他事情。

Worker 讓你能夠在一個不同的執行緒中執行一些任務,這樣你就可以啟動任務,然後繼續進行其他處理(例如處理使用者操作)。

所有這一切的一個問題是,如果多個執行緒可以訪問相同的共享資料,它們可能會獨立地、意外地(相對於彼此)更改它。這可能會導致難以發現的錯誤。

為了避免 Web 上的這些問題,你的主程式碼和 Worker 程式碼永遠無法直接訪問彼此的變數,並且只能在非常特定的情況下真正“共享”資料。Worker 和主程式碼執行在完全獨立的世界中,並且只能透過相互發送訊息進行互動。特別是,這意味著 Worker 無法訪問 DOM(視窗、文件、頁面元素等)。

有三種不同型別的 Worker:

  • 專用 Worker
  • 共享 Worker
  • 服務 Worker

在本文中,我們將透過一個第一種 Worker 的示例,然後簡要討論其他兩種。

使用 Web Worker

還記得第一篇文章中,我們有一個計算質數的頁面嗎?我們將使用 Worker 來執行質數計算,這樣我們的頁面就能對使用者操作保持響應。

同步質數生成器

讓我們首先回顧一下我們上一個示例中的 JavaScript 程式碼:

js
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/tree/main/javascript/asynchronous/workers/start 建立檔案的本地副本。此目錄中有四個檔案:

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

"index.html" 和 "style.css" 檔案已完成。

html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Prime numbers</title>
    <script 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>
css
textarea {
  display: block;
  margin: 1rem 0;
}

"main.js" 和 "generate.js" 檔案是空的。我們將把主程式碼新增到 "main.js",把 Worker 程式碼新增到 "generate.js"。

因此,首先我們可以看到 Worker 程式碼與主程式碼分開存放在一個單獨的指令碼中。我們還可以看到,從上面的 "index.html" 檔案來看,只有主程式碼包含在 <script> 元素中。

現在將以下程式碼複製到 "main.js" 中:

js
// Create a new worker, giving it the code in "generate.js"
const worker = new Worker("./generate.js");

// When the user clicks "Generate primes", send a message to the worker.
// The message command is "generate", and the message also contains "quota",
// which is the number of primes to generate.
document.querySelector("#generate").addEventListener("click", () => {
  const quota = document.querySelector("#quota").value;
  worker.postMessage({
    command: "generate",
    quota,
  });
});

// When the worker sends a message back to the main thread,
// update the output box with a message for the user, including the number of
// primes that were generated, taken from the message data.
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 一旦建立,Worker 指令碼就會立即執行。

  • 接下來,與同步版本一樣,我們為“生成質數”按鈕添加了一個 click 事件處理程式。但現在,我們不是呼叫 generatePrimes() 函式,而是使用 worker.postMessage() 向 Worker 傳送訊息。此訊息可以帶一個引數,在本例中,我們傳遞一個包含兩個屬性的 JSON 物件:

    • command:一個字串,用於標識我們希望 Worker 執行的操作(以防我們的 Worker 可以執行多項操作)
    • quota:要生成的質數數量。
  • 接下來,我們向 Worker 新增一個 message 事件處理程式。這樣 Worker 就可以告訴我們它何時完成,並向我們傳遞任何結果資料。我們的處理程式從訊息的 data 屬性中獲取資料,並將其寫入輸出元素(資料與 quota 完全相同,所以這有點無意義,但它展示了原理)。

  • 最後,我們實現了“重新載入”按鈕的 click 事件處理程式。這與同步版本完全相同。

現在是 Worker 程式碼。將以下程式碼複製到 "generate.js" 中:

js
// Listen for messages from the main thread.
// If the message command is "generate", call `generatePrimes()`
addEventListener("message", (message) => {
  if (message.data.command === "generate") {
    generatePrimes(message.data.quota);
  }
});

// Generate primes (very inefficiently)
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);
    }
  }

  // When we have finished, send a message to the main thread,
  // including the number of primes we generated.
  postMessage(primes.length);
}

請記住,這會在主指令碼建立 Worker 後立即執行。

Worker 所做的第一件事是開始監聽來自主指令碼的訊息。它使用 addEventListener() 來完成此操作,該函式是 Worker 中的一個全域性函式。在 message 事件處理程式內部,事件的 data 屬性包含從主指令碼傳遞的引數的副本。如果主指令碼傳遞了 generate 命令,我們會呼叫 generatePrimes(),並傳入來自訊息事件的 quota 值。

generatePrimes() 函式與同步版本類似,不同之處在於,它不是返回一個值,而是在完成時向主指令碼傳送一條訊息。我們為此使用 postMessage() 函式,該函式與 addEventListener() 一樣是 Worker 中的一個全域性函式。正如我們已經看到的,主指令碼正在監聽此訊息,並在收到訊息時更新 DOM。

注意:要執行此網站,您必須執行本地 Web 伺服器,因為不允許 file:// URL 載入 Worker。請參閱如何設定本地測試伺服器?以瞭解操作方法。完成此操作後,您應該能夠單擊“生成質數”並使您的主頁面保持響應。

如果您在建立或執行示例時遇到任何問題,可以檢視完成版本線上試用

其他型別的 Worker

我們剛剛建立的 Worker 稱為 專用 Worker。這意味著它由單個指令碼例項使用。

不過,還有其他型別的 Worker:

總結

在本文中,我們介紹了 Web Worker,它使 Web 應用程式能夠將任務分流到單獨的執行緒。主執行緒和 Worker 不直接共享任何變數,而是透過傳送訊息進行通訊,這些訊息由另一方作為 message 事件接收。

Worker 是一種保持主應用程式響應的有效方式,儘管它們無法訪問主應用程式可以訪問的所有 API,尤其無法訪問 DOM。

另見