跳至主要内容

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.resolvepromise.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 鏈中的處理。