JavaScript/非同步
在沒有特定關鍵字和技術的情況下,JavaScript 引擎會按照書寫程式碼的順序,依次執行語句。在大多數情況下,這是必要的,因為一行程式碼的結果會用在下一行程式碼中。
"use strict";
/* Line 1 */ const firstName = "Mahatma";
/* Line 2 */ const familyName = "Gandhi";
/* Line 3 */ const completeName = firstName + " " + familyName;
第 1 行和第 2 行必須完全完成,才能執行第 3 行。這是通常的順序行為。
但有些情況,後面的語句不需要等待當前語句執行結束。或者,你預期某個活動會執行很長時間,你想要在這期間做些別的事情。這種並行執行有可能顯著地減少整體響應時間。這是可能的,因為現代計算機擁有多個 CPU,能夠同時執行多個任務。在上面的例子中,第 1 行和第 2 行可以並行執行。此外,客戶端/伺服器架構在多個伺服器之間分配任務。
典型的情況是長時間執行的資料庫更新、處理大型檔案或 CPU 密集型計算。但即使對於渲染 HTML 頁面這樣看起來很簡單的事情,瀏覽器通常也執行多個任務。
原生,“JavaScript 是單執行緒的,所有 JavaScript 程式碼都在單個執行緒中執行。這包括你的程式原始碼和你包含在你程式中的第三方庫。當程式進行 I/O 操作來讀取檔案或網路請求時,這會阻塞主執行緒”[1]。
為了無論如何都實現同時活動的目標,瀏覽器、服務工作者、庫和伺服器提供了額外的適當介面。它們的初衷是解除 HTTP 和資料庫請求的阻塞執行;一小部分集中在 CPU 密集型計算上。
在 JavaScript 語言級別,有三種技術可以實現非同步 - 這感覺起來像同時性。
- 回撥函式
- Promise
- 關鍵字
async和await
基本技術是使用回撥函式。這個術語用於作為引數傳遞給另一個函式的函式,特別是 - 但不僅僅是 - 用於實現非同步行為的函式。
一個Promise 代表非同步操作的最終完成或失敗,包括其結果。它在這些非同步執行操作結束後,引導進一步處理。
因為使用 .then 和 .catch 對Promise 進行評估可能會導致程式碼難以閱讀 - 特別是如果它們巢狀 -,JavaScript 提供了關鍵字 async 和 await。它們的用法生成與傳統 JavaScript 中的 try .. catch .. finally 類似的井井有條的程式碼。但它們並沒有實現額外的功能。相反,在底層,它們是基於Promise。
為了證明程式碼並不總是按照嚴格的順序執行,我們使用一個指令碼,其中包含一個 CPU 密集型計算,它在一個或多或少很大的迴圈中執行。根據你的計算機,它會執行幾秒鐘。
"use strict";
// function with CPU-intensive computation
async function func_async(upper) {
await null; // (1)
return new Promise(function(resolve, reject) { // (2)
console.log("Starting loop with upper limit: " + upper);
if (upper < 0) {
// an arbitrary test to generate a failure
reject(upper + " is negative. Abort.");
}
for (let i = 0; i < upper; i++) {
// an arbitrary math function for test purpose
const s = Math.sin(i); // (3)
}
console.log("Finished loop for: " + upper);
resolve("Computed: " + upper);
})
}
const doTask = function(arr) {
for (let i = 0; i < arr.length; i++) {
console.log("Function invocation with number: " + arr[i]);
func_async(array1[i]) // (4)
.then((msg) => console.log("Ok. " + msg))
.catch((msg) => console.log("Error. " + msg));
console.log("Behind invocation for number: " + arr[i]);
}
}
const array1 = [3234567890, 10, -30];
doTask(array1);
console.log("End of program. Really?");
預期輸出
Function invocation with number: 3234567890
Behind invocation for number: 3234567890
Function invocation with number: 10
Behind invocation for number: 10
Function invocation with number: -30
Behind invocation for number: -30
End of program. Really?
Starting loop with upper limit: 3234567890
Finished loop for: 3234567890
Starting loop with upper limit: 10
Finished loop for: 10
Starting loop with upper limit: -30
Finished loop for: -30
Ok. Computed: 3234567890
Ok. Computed: 10
Error. -30 is negative. Abort.
- 非同步函式
func_async的核心是一個迴圈,其中進行了一次數學計算 [(3) 第 14 行]。迴圈需要多少時間取決於給定的引數。 func_async的返回值不是一個簡單值,而是一個Promise。[(2) 第 6 行]func_async針對給定陣列中的每個元素,被doTask呼叫一次。[(4) 第 24 行]- 由於
asyn_func的非同步性質,函式doTask在func_async執行之前就完全執行了!這可以透過程式輸出觀察到。 await null[(1) 第 5 行] 是一個虛擬呼叫。它會掛起func_async的執行,讓doTask繼續執行。如果你刪除這個語句,輸出結果將不同。結論:要讓函式真正非同步,你需要兩個關鍵字,函式簽名中的async和函式體中的await。- 如果你有一個工具可以詳細觀察你的計算機,你會發現
func_async的三個呼叫不是在不同的 CPU 上同時執行,而是在一個接一個地執行(在同一或不同的 CPU 上)。
將函式作為引數傳遞給(非同步)函式是 JavaScript 中的最初技術。我們將使用預定義的 setTimeout 函式來演示它的目的和優勢。它接受兩個引數。第一個是我們所說的回撥函式。第二個是指定回撥函式被呼叫後經過多少毫秒的一個數字。
"use strict";
function showMessageLater() {
setTimeout(showMessage, 3000); // in ms
}
function showMessage() {
alert("Good morning.");
}
showMessage(); // immediate invocation
showMessageLater(); // invocation of 'showMessage' after 3 seconds
如果 showMessage 被呼叫,它會立即執行。如果 showMessageLater 被呼叫,它會將 showMessage 作為回撥函式傳遞給 setTimeout,setTimeout 會在延遲 3 秒後執行它。
一個Promise 會跟蹤(非同步)函式是否已成功執行或已因錯誤終止,並確定接下來會發生什麼,呼叫 .then 或 .catch。
Promise 處於三種狀態之一
- 掛起:初始狀態,在“已解決”或“已拒絕”之前
- 已解決:函式成功完成之後:呼叫了
resolve - 已拒絕:函式因錯誤而完成之後:呼叫了
reject
"use strict";
// here, we have only the definition!
function demoPromise() {
// The 'return' statement is executed immediately, and the calling program
// continues its execution. But the value of 'return' keeps undefined until
// either 'resolve' or 'reject' is executed here.
return new Promise(function(resolve, reject) {
//
// perform some time-consuming actions like an
// access to a database
//
const result = true; // for example
if (result === true) {
resolve("Demo worked.");
} else {
reject("Demo failed.");
}
})
}
demoPromise() // invoke 'demoPromise'
.then((msg) => console.log("Ok: " + msg))
.catch((msg) => console.log("Error: " + msg));
console.log("End of script reached. But the program is still running.");
預期輸出
End of script reached. But the program is still running.
Ok: Demo worked.
當 demoPromise 被呼叫時,它會建立一個新的Promise 並返回它。然後,它會執行耗時的操作,並根據結果呼叫 resolve 或 reject。
接下來(在此期間,呼叫指令碼已執行了其他操作),demoPromise 呼叫後的 .then() 或 .catch() 函式中的一個會被執行。這兩個函式都接受一個(匿名)函式作為引數。傳遞給匿名函式的引數是Promise 的值。
.then( ( msg ) => console.log("Ok: " + msg) )
| | | | | | | | | |
| | | └── parameter ──┘ └────── funct. body ────┘ | | |
| | | | | |
| | └───────────── anonymous function ───────────────┘ | |
| | | |
| └─────── parameter of then() function ───────────────┘ |
| |
└─────────────────── then() function ──────────────────────┘
請注意,指令碼的最後一條語句已之前執行了。
許多庫、API 和伺服器函式的介面都是定義和實現為返回一個Promise 的函式,類似於上面的 demoPromise。只要你不必建立自己的非同步函式,你就沒有必要建立Promise。通常,呼叫外部介面並只使用 .then 或 .catch(或下一章的 async 或 await)就足夠了。
關鍵字 await 會強制 JavaScript 引擎在執行下一條語句之前,完全執行 await 後面的指令碼 - 包括 new Promise 部分。因此 - 從呼叫指令碼的角度來看 - 非同步行為被去除了。帶有 await 的函式必須在它們的簽名中用關鍵字 async 標記。
"use strict";
// same as above
function demoPromise() {
return new Promise(function(resolve, reject) {
const result = true; // for example
if (result === true) {
resolve("Demo worked.");
} else {
reject("Demo failed.");
}
})
}
// a function with the call to 'demoPromise'
// the keyword 'async' is necessary to allow 'await' inside
async function start() {
try {
// use 'await' to wait for the end of 'demoPromise'
// before executing the following statement
const msg = await demoPromise();
// without 'await', 'msg' contains the Promise, but without
// the success- or error-message
console.log("Ok: " + msg);
} catch (msg) {
console.log("Error: " + msg);
}
}
start();
console.log("End of script reached. End of program?");
使用 async .. await 允許你使用傳統的 try .. catch 語句,而不是 .then 和 .catch。
我們使用免費提供的演示 API https://jsonplaceholder.typicode.com/。它以 JSON 格式提供少量測試資料。
"use strict";
async function getUserData() {
// fetch() is an asynchronous function of the Web API. It returns a Promise
// see: https://mdn.club.tw/en-US/docs/Web/API/Fetch_API/Using_Fetch
await fetch('https://jsonplaceholder.typicode.com/users')
// for the use of 'response', see: https://mdn.club.tw/en-US/docs/Web/API/Response/json
// '.json()' reads the response stream
.then((response) => { return response.json() })
// in this case, 'users' is an array of objects (JSON format)
.then((users) => {
console.log(users); // total data: array with 10 elements
// loop over the ten array elements
for (const user of users) {
console.log(user.name + " / " + user.email);
}
})
.catch((err) => console.log('Some error occurred: ' + err.message));
}
// same with 'try / catch'
async function getUserData_tc() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await response.json();
console.log(users);
console.log(users[0].name);
for (const user of users) {
console.log(user.name + " / " + user.email);
}
} catch (err) {
console.log('Some error occurred: ' + err.message);
}
}
getUserData();
getUserData_tc();
此例子的步驟為
await fetch():獲取 URL 後面的資料。await部分保證指令碼在fetch傳遞所有資料之前不會繼續。json()讀取包含結果資料的流。- 結果資料是一個包含 10 個元素的陣列。每個元素都以 JSON 格式,例如
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
}
- 作為示例,該指令碼在控制檯中顯示了完整資料及其部分內容。
注意:當你使用任意 URL 時,你可能會遇到一個CORS 錯誤。