介紹 Worker

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

先決條件 對 JavaScript 基礎知識(包括事件處理)有基本的瞭解。
目標 瞭解如何使用 Web Workers。

在本模組的第一篇文章中,我們看到了在程式中執行長時間同步任務時會發生什麼 - 整個視窗完全無響應。從根本上說,出現這種情況的原因是程式是*單執行緒*的。一個*執行緒*是程式執行的一系列指令。由於程式僅包含一個執行緒,它一次只能執行一項操作:因此,如果它正在等待我們的長時間同步呼叫返回,它將無法執行任何其他操作。

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

從這一切中,我們有一個擔憂,如果多個執行緒可以訪問相同的共享資料,它們可能會獨立且意外地(相對於彼此)更改資料。這會導致難以查詢的錯誤。

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

有三種不同型別的 Workers

  • 專用 Workers
  • 共享 Workers
  • 服務 Workers

在本文中,我們將透過第一個型別 Workers 的示例進行說明,然後簡要討論另外兩種型別。

使用 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()來完成此操作,addEventListener()是 Worker 中的全域性函式。在message事件處理程式中,事件的data屬性包含從主指令碼傳遞的引數的副本。如果主指令碼傳遞了generate命令,我們將呼叫generatePrimes(),並將來自訊息事件的quota值傳遞給它。

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

注意:要執行此站點,您必須執行本地 Web 伺服器,因為不允許 file:// URL 載入 Workers。請參閱我們關於設定本地測試伺服器的指南。完成此操作後,您應該能夠單擊“生成素數”,並且您的主頁面將保持響應。

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

其他型別的 Worker

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

但是還有其他型別的 Workers

結論

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

Workers 可以有效地保持主應用程式的響應,儘管它們無法訪問主應用程式可以訪問的所有 API,特別是無法訪問 DOM。

另請參閱