Javascript 的非同步操作(一)
同步與非同步
當我們談論 JavaScript 中的同步和非同步時,我們指的是程式執行的方式。
同步(Synchronous): JavaScript 是一個單線程(single threaded)的語言,這意味著同一時間只能執行一段程式碼,並按照它們出現的順序執行。在同步環境中,程式將按照它們在腳本中的順序執行。每個操作都必須等待前一個操作完成後才能繼續。
console.log('Step 1');
console.log('Step 2');
console.log('Step 3');
// Step 1
// Step 2
// Step 3
非同步(Asynchronous): 在非同步環境中,程式執行不會等待前一個操作完成。這允許程式同時執行其他操作,而不阻塞後續程式的執行。非同步操作在處理需要等待的任務時非常有用,例如網路請求、檔案讀寫等。
在以下例子中,Step 1 和 Step 3 可能會在 Step 2 完成之前執行,因為 setTimeout 會將回調函式推送到 Web API 中,而不會阻塞主線程的執行。
console.log('Step 1');
setTimeout(() => {
console.log('Step 2');
}, 1000);
console.log('Step 3');
// Step 1
// Step 3
// Step 2
簡而言之,同步是按照順序執行的方式,而非同步允許某些操作在背後執行,從而提高應用程序的性能和響應性。 Web API 提供了許多非同步操作,如 setTimeout、setInterval、以及與 DOM 互動的事件處理程序。
Promise
Promise 是 ES6 後出現的語法。 當某個操作需要花費較長時間,比如從網絡請求數據、讀取文件,或是進行計算密集型的任務時,同步操作會阻塞程式的執行,此時就需要非同步操作來避免阻塞。
當需要處理非同步操作時,JavaScript 提供了兩種主要的方式來處理:Promise 和 async/await。本篇會聚焦在 Promise,async/await 會在下一篇文章繼續討論。
Promise 的基本狀態與方法
Promise 有三種基本狀態:
- Pending(進行中): Promise 的初始狀態。表示當前操作還在進行中,尚未完成。
- Fulfiled(已完成): 表示當前操作已經成功完成。這時會調用
then
方法。 - Rejected(已拒絕): 表示當前操作未能成功完成。這時會調用
catch
方法。
Promise 的基本方法如下:
then
: 這是 Promise 最常用的方法之一,用於處理 Promise 成功的情況。它接受兩個參數,第一個是成功時的回調函數,第二個是失敗時的回調函數(Optional)。
promise.then(
function(result) {
// 處理成功的情況
},
function(error) {
// 處理失敗的情況
}
);
catch
: 用於處理 Promise 的失敗情況。它是 then 方法的一個簡短形式,只處理失敗的回調。
promise.catch(function(error) {
// 處理失敗的情況
});
finally
: 無論 Promise 是成功還是失敗,都會執行 finally 中指定的回調函數。這在需要在 Promise 完成後執行某些清理或善後操作時很有用。
promise.finally(function() {
// 不管成功還是失敗,都會執行的操作
});
Promise 的範例
範例一
以下是基本的 Promise 範例
// 創建一個 Promise
const myPromise = new Promise((resolve, reject) => {
// 非同步操作
setTimeout(() => {
const success = true; // 模擬成功或失敗
if (success) {
resolve("成功");
} else {
reject(Error("失敗"));
}
}, 1000);
});
// 使用 Promise
myPromise
.then((result) => {
console.log(result); // "成功"
})
.catch((err) => {
console.log(err); // Error: 失敗
});
- P.S. 如果一個箭頭函數只有一個參數,你可以省略掉括號。像是如果上述範例的 (resolve, reject) 如果只剩 (resolve) 時,可以省略成 resolve。
範例二
我們打 API 時,使用 fetch 函式本身就會回傳一個 Promise 對象,因此不需要手動創建 Promise。
// 定義一個函式,使用 fetch 來獲取使用者數據
const fetchUserData = () => {
// 返回 fetch 的 Promise 對象
return fetch('https://api.example.com/user')
.then((response) => {
// 檢查響應的狀態碼
if (!response.ok) {
throw new Error('無法獲取使用者數據');
}
// 使用 .json() 解析 JSON 數據,並返回新的 Promise 對象
return response.json();
})
.then((userData) => {
// 返回獲取的使用者數據
return userData;
})
.catch((error) => {
// 處理錯誤
console.error('API 請求失敗:', error.message);
throw error; // 可以選擇拋出錯誤供外部處理
});
};
// 呼叫函式
fetchUserData()
.then((userData) => {
console.log('成功獲取使用者數據:', userData);
})
.catch((error) => {
console.error('獲取使用者數據失敗:', error.message);
});
LeetCode: Sleep
以下是一個 LeetCode 的 Easy 題,可以利用上述 Promise 的方法來解決,,題目敘述如下:
Given a positive integer millis, write an asynchronous function that sleeps for millis milliseconds. It can resolve any value.
Example
// Input:
millis = 100
// Output:
// 100
Explanation: It should return a promise that resolves after 100ms.
let t = Date.now();
sleep(100).then(() => {
console.log(Date.now() - t); // 100
});
解題
- 可以在 sleep 函式裡使用 setTimeout 來實現時間的延遲,當時間達到指定的 millis 時會返回一個 Promise,它的 resolve 函式將在指定的時間後被呼叫。
- 接著在 sleep 的 then 函式中,計算從 Date.now() 到 resolve 被呼叫的時間間隔,以確認實際休眠的時間。
const sleep = (millis) => {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, millis)
})
}
let t = Date.now()
sleep(100).then(() => console.log(Date.now() - t)) // 100
let t2 = Date.now();
sleep(200).then(() => {
console.log(Date.now() - t2); // 200
});
- 雖然本題在 resolve 裡是沒有值的,但在創建 Promise 時還是需要有 resolve,因為在異步操作完成後會呼叫 resolve 來通知 Promise 解析。如果 resolve 沒有被呼叫,Promise 將一直保持為待定狀態而不會進入到 then 函式。
補充:Promise 的靜態方法
如前面提到的 Promise 有三種狀態,針對 resolve 與 reject 的情況,還可以使用 promise.resolve
與 promise.reject
這樣 Promise 的一個靜態方法 (static methods)。
像是處理已完成(fulfilled)的 Promise 實例時,可以用以下方式:
const myPromise = Promise.resolve("成功");
上述程式碼等同於:
const myPromise = new Promise((resolve) => {
resolve("成功");
});
Promise Chaining
Promise Chaining 是一種使用 Promise 的模式,允許在異步操作完成後,依次執行一系列的異步操作。這樣的方式可以更好地處理異步程式碼,使其看起來更加清晰和易讀。
基本模式如下:
asyncFunction()
.then(result1 => {
// Do something with result1
return anotherAsyncFunction(result1);
})
.then(result2 => {
// Do something with result2
return yetAnotherAsyncFunction(result2);
})
.then(finalResult => {
// Do something with the final result
})
.catch(error => {
// Handle errors in the chain
});
每個 .then 都返回一個新的 Promise,這樣就可以繼續鏈接下一個 .then。如果在鏈中的任何地方出現錯誤,將會跳轉到 .catch 部分。
Promise.all
Promise.all 可以在一次調用中同時處理多個 Promise。它接收一個包含多個 Promise 的數組,並返回一個新的 Promise。這個新的 Promise 在數組中的所有 Promise 都完成後被解析,解析值是包含所有 Promise 解析值的數組。
const promise1 = fetch('https://api.example.com/data/1');
const promise2 = fetch('https://api.example.com/data/2');
Promise.all([promise1, promise2])
.then(results => {
// Handle the results array
})
.catch(error => {
// Handle errors in any of the promises
});
Promise.race
Promise.race 可以在多個 Promise 中選擇第一個解析或拒絕的 Promise。它同樣接收一個包含多個 Promise 的數組,並返回一個新的 Promise。這個新的 Promise 在數組中的任何一個 Promise 完成時解析或拒絕。
const promise1 = fetch('https://api.example.com/data/1');
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 1000, 'Timeout'));
Promise.race([promise1, promise2])
.then(result => {
// Handle the first resolved promise
})
.catch(error => {
// Handle the first rejected promise or timeout
});
Promise.all 的範例
以下是一個 easy 的 LeetCode 題,題目如下:
Given two promises promise1
and promise2
, return a new promise. promise1 and promise2 will both resolve with a number. The returned promise should resolve with the sum of the two numbers.
Example:
// Input:
promise1 = new Promise(resolve => setTimeout(() => resolve(2), 20));
promise2 = new Promise(resolve => setTimeout(() => resolve(5), 60));
// Output:
// 7
Explanation:
The two input promises resolve with the values of 2 and 5 respectively. The returned promise should resolve with a value of 2 + 5 = 7. The time the returned promise resolves is not judged for this problem.
解題
看到這個題目時,可能會想說使用 Promise.all
來處理兩個 promise,概念如下:
Promise.all([promise1, promise2])
.then(([result1, result2]) => {
return result1 + result2;
})
那現在把他寫成函式:
const addTwoPromises = (promise1, promise2) => {
return Promise.all([promise1, promise2])
.then(([result1, result2]) => {
return result1 + result2;
})
};
// 使用範例
const promise1 = new Promise((resolve) => setTimeout(() => resolve(2), 20));
const promise2 = new Promise((resolve) => setTimeout(() => resolve(5), 60));
addTwoPromises(promise1, promise2)
.then((result) => {
console.log(result); // 7
})
為什麼第一到四行會有兩個 return 呢?
Promise.all 返回的是一個 Promise,這個 Promise 會在所有傳入的 Promises 都完成後被 resolve。
.then() 會在前一個 Promise(這裡是 Promise.all 的返回值)被 resolve 後執行。在 .then() 中,我們使用解構賦值(Destructuring Assignment)取得 result1 和 result2,然後計算它們的和,並使用 return 返回這個和。這個 return 會影響到下一個 .then() 或 Promise 鏈中的處理。