[JavaScript] Promise 用法與常見的練習題型

Sean Chou
Recording everything
17 min readJun 15, 2024

--

from: https://www.pexels.com/photo/a-close-up-shot-of-people-in-agreement-6077239/

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

Pending:初始狀態
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’
Fulfilled:執行完成狀態
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’
Rejected:執行失敗狀態

幾種 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:

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
AggregateError: { errors, message, stack }

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.allSettled() 回傳的結果

最後總結一下:

  • 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/

from: https://leetcode.com/studyplan/30-days-of-javascript/

相加兩個 Promise 的值

題目連結:2723. Add Two Promises

題目解析

建立一個 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));
}

檢查執行時間

題目連結:2637. Promise Time Limit

題目解析

輸入一個非同步的函式 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))
}
})
};

--

--