介紹 Worker
在“非同步 JavaScript”模組的最後一篇文章中,我們將介紹 worker,它使你能夠在單獨的執行緒中執行某些任務。
| 預備知識 | 對本模組前面課程中介紹的 JavaScript 基礎知識和非同步概念有紮實的理解。 |
|---|---|
| 學習成果 |
|
在本模組的第一篇文章中,我們看到了當程式中有一個長時間執行的同步任務時會發生什麼——整個視窗變得完全無響應。從根本上說,這是因為程式是 單執行緒 的。 執行緒 是程式遵循的一系列指令。由於程式由單個執行緒組成,它一次只能做一件事:因此,如果它正在等待我們長時間執行的同步呼叫返回,它就不能做任何其他事情。
Worker 讓你能夠在一個不同的執行緒中執行一些任務,這樣你就可以啟動任務,然後繼續進行其他處理(例如處理使用者操作)。
所有這一切的一個問題是,如果多個執行緒可以訪問相同的共享資料,它們可能會獨立地、意外地(相對於彼此)更改它。這可能會導致難以發現的錯誤。
為了避免 Web 上的這些問題,你的主程式碼和 Worker 程式碼永遠無法直接訪問彼此的變數,並且只能在非常特定的情況下真正“共享”資料。Worker 和主程式碼執行在完全獨立的世界中,並且只能透過相互發送訊息進行互動。特別是,這意味著 Worker 無法訪問 DOM(視窗、文件、頁面元素等)。
有三種不同型別的 Worker:
- 專用 Worker
- 共享 Worker
- 服務 Worker
在本文中,我們將透過一個第一種 Worker 的示例,然後簡要討論其他兩種。
使用 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() 來完成此操作,該函式是 Worker 中的一個全域性函式。在 message 事件處理程式內部,事件的 data 屬性包含從主指令碼傳遞的引數的副本。如果主指令碼傳遞了 generate 命令,我們會呼叫 generatePrimes(),並傳入來自訊息事件的 quota 值。
generatePrimes() 函式與同步版本類似,不同之處在於,它不是返回一個值,而是在完成時向主指令碼傳送一條訊息。我們為此使用 postMessage() 函式,該函式與 addEventListener() 一樣是 Worker 中的一個全域性函式。正如我們已經看到的,主指令碼正在監聽此訊息,並在收到訊息時更新 DOM。
注意:要執行此網站,您必須執行本地 Web 伺服器,因為不允許 file:// URL 載入 Worker。請參閱如何設定本地測試伺服器?以瞭解操作方法。完成此操作後,您應該能夠單擊“生成質數”並使您的主頁面保持響應。
其他型別的 Worker
我們剛剛建立的 Worker 稱為 專用 Worker。這意味著它由單個指令碼例項使用。
不過,還有其他型別的 Worker:
- 共享 Worker 可以由在不同視窗中執行的幾個不同指令碼共享。
- Service Worker 充當代理伺服器,快取資源,以便 Web 應用程式在使用者離線時也能工作。它們是漸進式 Web 應用程式的關鍵元件。
總結
在本文中,我們介紹了 Web Worker,它使 Web 應用程式能夠將任務分流到單獨的執行緒。主執行緒和 Worker 不直接共享任何變數,而是透過傳送訊息進行通訊,這些訊息由另一方作為 message 事件接收。
Worker 是一種保持主應用程式響應的有效方式,儘管它們無法訪問主應用程式可以訪問的所有 API,尤其無法訪問 DOM。