[JavaScript] Promise 用法與常見的練習題型
Promise 是一個表示非同步運算的最終完成或失敗的物件,從 ES6 之後開始出現,主要為了解決在非同步操作時,會出現大量 callback 造成的問題,是現今前端開發不可不知道的一個概念,也常常是面試的熱門考題之一。
先來看看以前在非同步處理的時候,如果需要等待結果回來後做處理,就必須要使用層層 callback function 結合的方式,舉例來說:
setTimeout(function () {
console.log("1");
setTimeout(function () {
console.log("2");
setTimeout(function () {
console.log("3");
}, 3000);
}, 2000);
}, 1000);
當如果要做的事情越來越多,這樣可讀性也會越來越差,這種現象通常也被稱為 callback hell,或是波動拳問題。
基本概念
Promise, to tell someone that you will certainly do something
允諾,答應,保證
Promise 字面意思就是「保證」,不論成功或失敗都會告訴你一個狀態。
主要分為三種狀態:
- Pending:初始狀態
- Fulfilled:執行完成狀態
- Rejected:執行失敗狀態
在 Promise 建立的時候會先進入 Pending
狀態,如果執行成功就會進入 Fulfilled
,反之失敗就進入 Rejected
。
const promise = new Promise((resolve, reject) => {
setTimeout(()=> {
resolve('success');
}, 3000)
});
promise.then((result) => {
console.log(result);
}).catch(() => {
console.log(result);
});
- 建立後先進入
Pending
- 3秒後會進入
Fulfilled
,執行 resolve() - promise 會執行 then(),印出 ‘success’
const promise = new Promise((resolve, reject) => {
setTimeout(()=> {
reject('failed');
}, 3000)
});
promise.then((result) => {
console.log(result);
}).catch((error) => {
console.log(error);
});
- 建立後先進入
Pending
- 3秒後會進入
Rejected
,執行 reject() - promise 會執行 catch(),印出 ‘failed’
幾種 Promise 用法
Promise 也有一些靜態方法可以使用,讓我們可以搭配處理更複雜多元的非同步情境,以下列出幾個可能比較常用到的:
Promise.all()
Promise.all()
方法回傳一個Promise
物件,當引數iterable
中所有的 promises 都被實現(resolved),或引數 iterable 不含任何 promise 時,被實現。或以第一個被拒絕的 promise 的原因被拒絕。from: https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
只有全部的 promise 成功,才會回傳成功,任一個失敗就會直接失敗。
Resolved:
- 當所有的 promises 都被實現 (resolved) 後才算 resolved
- 所有 promises 都 resolved 後,才會回傳一個包含結果的陣列
var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
});
Promise.all([p1, p2, p3]).then((values) => {
console.log(values);
});
//From console:
// [3, 1337, "foo"]
// from: https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
Rejected:
- 只要有任何一個 promise 被拒絕 (rejected) 就立即結束
- 回傳被拒絕那個 promises 的 error
var p1 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, "1");
});
var p2 = new Promise((resolve, reject) => {
setTimeout(resolve, 2000, "2");
});
var p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 3000, "3");
});
var p4 = new Promise((resolve, reject) => {
setTimeout(resolve, 4000, "4");
});
var p5 = new Promise((resolve, reject) => {
reject("reject");
});
Promise.all([p1, p2, p3, p4, p5])
.then((values) => {
console.log(values);
})
.catch((reason) => {
console.log(reason);
});
//From console:
//"reject"
Promise.race()
Promise.race(iterable)
方法回傳一個 promise 物件,此 promise 物件會於 iterable 引數中任一個 promise 轉為 resolve 或 rejected 時立即轉變成 resolve 或 rejected,並且接收其成功值或失敗訊息。from: https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
以第一個執行完的 promises 結果為主,只回傳第一個執行完的結果。
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, "one");
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 100, "two");
});
Promise.race([p1, p2]).then(function (value) {
console.log(value); // "two"
// Both resolve, but p2 is faster
});
var p3 = new Promise(function (resolve, reject) {
setTimeout(resolve, 100, "three");
});
var p4 = new Promise(function (resolve, reject) {
setTimeout(reject, 500, "four");
});
Promise.race([p3, p4]).then(
function (value) {
console.log(value); // "three"
// p3 is faster, so it resolves
},
function (reason) {
// Not called
},
);
var p5 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, "five");
});
var p6 = new Promise(function (resolve, reject) {
setTimeout(reject, 100, "six");
});
Promise.race([p5, p6]).then(
function (value) {
// Not called
},
function (reason) {
console.log(reason); // "six"
// p6 is faster, so it rejects
},
);
// from: https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
Promise.any()
Promise.any() 靜態方法採用可迭代的 Promise 作為輸入並傳回單一 Promise。當任何輸入的 Promise resolve 時,返回的 Promise 就會 resolve,並具有第一個 resolve值。當所有輸入的 Promise 都 rejected 時(包括傳遞空的可迭代物件時),它會 rejected,並且 AggregateError 包含拒絕原因陣列。
from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any
只要有任何一個 promises 最先執行成功,則回傳此 promises 執行完的結果。如果全部都失敗。
Resolved:
- 任何一個 promises 執行成功
- 回傳成功的那一個 promises value
const promise1 = new Promise((resolve, reject) => {
reject("fails 1");
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, "resolve 1");
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "resolve 2");
});
Promise.any([promise1, promise2, promise3]).then((value) => {
console.log(value);
});
//From console:
// resolve 2
Rejected:
- 當所有 promises 都失敗
- 回傳 AggregateError (請見 MDN: AggregateError)
const promise1 = new Promise((resolve, reject) => {
reject("fails 1");
});
const promise2 = new Promise((resolve, reject) => {
reject("fails 2");
});
const promise3 = new Promise((resolve, reject) => {
reject("fails 3");
});
Promise.any([promise1, promise2, promise3]).then((value) => {
console.log(value);
});
//From console:
// AggregateError
Promise.allSettled()
Promise.allSettled() 靜態方法採用可迭代的 Promise 作為輸入並傳回單一 Promise。當所有輸入的 promise 都 resolve 時(包括傳遞空的可迭代物件時),傳回的 promise 就會履行,並帶有描述每個 promise 結果的物件陣列。
from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
所有 promises 都執行完才會一起回傳結果,不論成功或是失敗的結果都會回傳,會多一個 status 的參數。
const promise1 = new Promise((resolve, reject) => {
reject("fails 1");
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, "resolve 1");
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "resolve 2");
});
Promise.allSettled([promise1, promise2, promise3]).then((value) => {
console.log(value);
});
/*
From console:
[
{"status":"rejected","reason":"fails 1"},
{"status":"fulfilled","value":"resolve 1"},
{"status":"fulfilled","value":"resolve 2"}
]
*/
最後總結一下:
- Promise.all(): 只有全部的 promise 成功,才會回傳成功,任一個失敗就會直接失敗
- Promise.allSettled(): 等待全部的 promise 執行結束,不論成功還是失敗都回傳
- Promise.any(): 回傳先「成功」的 promise
- Promise.race(): 回傳先「結束」的 promise
常見考題
leetcode 網站上有一個 JS 30 的 30 題 JavaScript 手寫題,蠻適合拿來做基礎 JavaScript 的觀念複習,以下也是從裡面挑出與 Promise 相關的題目。
JS 30
https://leetcode.com/studyplan/30-days-of-javascript/
相加兩個 Promise 的值
題目解析
建立一個 addTwoPromises 的 function,可以相加兩個 promise 的值。
解題方法
這題很明確的就是要考會不會使用 Promise.all() 這個靜態方法,如果知道是要使用 Promise.all() 的話,這題就完全沒有難度了,可以算是一個驗證會不會使用基本靜態方法的考題。
/**
* @param {Promise} promise1
* @param {Promise} promise2
* @return {Promise}
*/
var addTwoPromises = async function(promise1, promise2) {
return Promise.all([promise1, promise2]).then(([res1, res2]) => res1 + res2)
};
實作一個 Sleep function
題目連結:2621. Sleep
題目解析
建立一個 sleep function,輸入特定毫秒數,可以使用這個 sleep function,達成等待特定表述的效果,例如:
三秒之後才印出 console.log
sleep(3000).then(() => { console.log(‘resolved’) })
直接停三秒
await sleep(3000);
解題方法
這題主要要搭配 setTimeout 來一起使用,透過 Promise 的特性,讓 resolve 在特定秒數之後才會被執行,達成等待的效果。
/**
* @param {number} millis
* @return {Promise}
*/
async function sleep(millis) {
return new Promise((resolve) => setTimeout(resolve, millis));
}
檢查執行時間
題目解析
輸入一個非同步的函式 fn
與一個時間 t
,提供一個 timeLimit 的檢查方法,驗證函式有沒有在時間內完成。
- 在 t 秒內完成,回傳函式的結果
- 在 t 秒內無法完成,rejected 且回傳 “Time Limit Exceeded”
解題方法
思考一下題意,可以發現函式有沒有在時間內完成,可以轉化成,我們只要找到時間 t
與函式 fn
誰先執行完成即可,如果 t
先達到了,代表函式超時,這時候就 rejected,如果函式 fn
先完成了,就回傳原來的值。
我們可以使用靜態方法 Promise.race()
- Promise.race(): 回傳先「結束」的 promise
/**
* @param {Function} fn
* @param {number} t
* @return {Function}
*/
var timeLimit = function(fn, t) {
return async function(...args) {
const rejectPromise = new Promise((resolve, reject) => {
setTimeout(() => reject('Time Limit Exceeded'), t)
});
const resolvePromise = fn(...args);
return Promise.race([rejectPromise, resolvePromise])
}
};
實作 Promise All
題目連結:2721. Execute Asynchronous Functions in Parallel
題目解析
這題雖然題目很長,但其實就是要你實作出 Promise.all()
解題方法
/**
* @param {Array<Function>} functions
* @return {Promise<any>}
*/
var promiseAll = async function(functions) {
return new Promise((resolve, reject) => {
let count = 0;
let result = new Array(functions.length)
for(let i=0; i<functions.length; i++){
functions[i]().then((data) =>{
count++;
result[i] = data
if(count == functions.length) return resolve(result)
})
.catch((err) => reject(err))
}
})
};