介紹 Worker
在本模組的最後一篇文章“非同步 JavaScript”中,我們將介紹*workers*,它使您能夠在單獨的執行緒中執行一些任務。
| 先決條件 | 對 JavaScript 基礎知識(包括事件處理)有基本的瞭解。 |
|---|---|
| 目標 | 瞭解如何使用 Web Workers。 |
在本模組的第一篇文章中,我們看到了在程式中執行長時間同步任務時會發生什麼 - 整個視窗完全無響應。從根本上說,出現這種情況的原因是程式是*單執行緒*的。一個*執行緒*是程式執行的一系列指令。由於程式僅包含一個執行緒,它一次只能執行一項操作:因此,如果它正在等待我們的長時間同步呼叫返回,它將無法執行任何其他操作。
Workers 使您能夠在不同的執行緒中執行一些任務,這樣您就可以啟動任務,然後繼續進行其他處理(例如處理使用者操作)。
從這一切中,我們有一個擔憂,如果多個執行緒可以訪問相同的共享資料,它們可能會獨立且意外地(相對於彼此)更改資料。這會導致難以查詢的錯誤。
為了避免在 Web 上出現這些問題,您的主程式碼和 worker 程式碼永遠不會直接訪問彼此的變數,並且只能在非常特定的情況下真正“共享”資料。Workers 和主程式碼在完全獨立的環境中執行,並且只通過互相傳送訊息進行互動。特別是,這意味著 Workers 無法訪問 DOM(視窗、文件、頁面元素等)。
有三種不同型別的 Workers
- 專用 Workers
- 共享 Workers
- 服務 Workers
在本文中,我們將透過第一個型別 Workers 的示例進行說明,然後簡要討論另外兩種型別。
使用 Web Worker
還記得我們在第一篇文章中,有一個頁面計算素數嗎?我們將使用 Worker 來執行素數計算,這樣我們的頁面就能保持對使用者操作的響應。
同步素數生成器
讓我們首先再次看一下我們之前示例中的 JavaScript 程式碼
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”檔案已經完成
<!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>
textarea {
display: block;
margin: 1rem 0;
}
“main.js”和“generate.js”檔案為空。我們將把主程式碼新增到“main.js”中,將 Worker 程式碼新增到“generate.js”中。
因此首先,我們可以看到 Worker 程式碼與主程式碼儲存在不同的指令碼中。我們還可以看到,從上面的“index.html”可以看出,只有主程式碼包含在<script>元素中。
現在將以下程式碼複製到“main.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”中
// 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。
其他型別的 Worker
我們剛剛建立的 Worker 被稱為專用 Worker。這意味著它被單個指令碼例項使用。
但是還有其他型別的 Workers
- 共享 Workers可以被在不同視窗中執行的多個不同指令碼共享。
- 服務 Workers充當代理伺服器,快取資源,以便 Web 應用程式可以在使用者離線時工作。它們是漸進式 Web 應用程式的關鍵元件。
結論
在本文中,我們介紹了 Web Workers,它使 Web 應用程式能夠將任務解除安裝到單獨的執行緒。主執行緒和 Worker 不直接共享任何變數,而是透過傳送訊息進行通訊,這些訊息以message事件的形式被另一方接收。
Workers 可以有效地保持主應用程式的響應,儘管它們無法訪問主應用程式可以訪問的所有 API,特別是無法訪問 DOM。