JavaScript 中的 Promise 是什麼?以及為什麼你要懂 Promise

前言

那麼這一篇會來談談什麼是 Promise 以及 Async/Await 語法。

Promise 概念

Promise 其實就是承諾的意思,當然不是指 Promise 中文翻譯是承諾這件事,如果有這麼簡單我也不用特別寫成一篇文章。

那麼先來講講為什麼你要懂 Promise 這件事情。

你在開發時應該很常遇到所謂的「非同步」事件,例如以下程式碼預期應該是要依照順序跑:

1
2
3
4
5
6
7
console.log('Start');

setTimeout(() => {
console.log('My Name is Ray');
}, 0);

console.log('End');

但實際上結果卻是…

1
2
3
1. Start
2. End
3. My Name is Ray

那這件事情與 Promise 有什麼關聯性呢?實際開發來講,我們往往會預期希望結果是依照我們要的順序去運作,畢竟程式碼亂跳的話結果也一定會不同。

那 Promise 概念是什麼呢?雖然中文翻譯是一個承諾的意思,但實際上是如何呢?我們試著用比較現實生活層面來舉例。

你今天去人超多的百貨公司買一點小吃,例如…繼光香香雞

繼光香香雞(圖源網路)

當你點好了繼光香香雞後店員給你了一個取餐單或者是取餐叫號器

小圓盤(圖源網路)

等你拿到那一張取餐單 or 叫號器之後,你就會等到它叫你去取餐。

當然這段時間你依然可以去做點其他事情,只是你等一下會回頭處理他的事情,只是你一定會是先去繼光香香雞點餐後再去取餐

所以你可以理解到一件事情 Promise 簡單來講就是我答應你我等一下一定會做什麼事情。

那 Promise 該如何撰寫呢?讓我們繼續往下看。

Promise

Promise 本身是一個建構函式,因此會需要使用到 new 語法來實例化它,而它主要會帶入兩個參數,分別是

  • resolve - 成功
  • reject - 失敗
1
new Promise(function (resolve, reject) {});

而宣告方式有兩種

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 第一種
const myName = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray');
}, 300);
})
}

// 第二種
function myName () {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray');
}, 300);
})
}

而這兩者寫法並沒有太大差異,所以這邊主要看你自己需求來調整撰寫。

Promise 使用

而 Promise 的使用方式也相當簡單,當你宣告了一個 Promise 後,你可以使用 thencatch 串接 Promise 回傳的結果

1
2
3
4
5
6
7
8
9
10
11
const myName = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray');
}, 300);
})
}

myName()
.then((res) => console.log('成功:'+ res))
.catch((error) => console.log('失敗:' + error));

那麼問題來了,什麼時候會跑 then 什麼時候會跑 catch 呢?其實這是我們可以決定的,前面我們有講到實例化 Promise 時會帶入兩個參數,分別是 resolvereject,而當若結果回傳的是 resolve 則會跑 then,反之若是 reject 就是 catch

Promise 靜態方法

除此之外 Promise 也有提供一些靜態方法(不需要實例化即可使用的方法):

  • Promise.all(iterable)
  • Promise.race(iterable)
  • Promise.reject(resason)
  • Promise.resolve(value)

Promise.all

當你有多個 Promise 事件需要一起執行與完成時,就可以使用 Promise.all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const myName1 = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray1');
}, 300);
})
}

const myName2 = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray2');
}, 300);
})
}

Promise.all([myName1(), myName2()])
.then((res) => {
console.log(res); // ['Ray1', 'Ray2']
})

這邊需要注意回傳的結果會與你一開始傳入的順序是相同的,而這個方法很適合用於打多隻 API 時。

Promise.race

Promise.race 簡單來講就是,當然當有任一個事件完成時,就會只回傳那一個事件,舉例來講 myName2 將會比 myName1 更快結束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const myName1 = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray1');
}, 1000);
})
}

const myName2 = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray2');
}, 100);
})
}

Promise.race([myName1(), myName2()])
.then((res) => {
console.log(res); // Ray2
})

Promise.reject & Promise.resolve

最後是關於 Promise.rejectPromise.resolve 的部分,這兩個方法就是只取得成功或是失敗得結果而已

1
2
3
4
5
6
7
8
9
const myName = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray');
}, 300);
})
}

Promise.resolve(myName()).then(res => console.log(res))

反之若改成 Promise.reject 則必定回傳失敗結果。

其他方法

最後其實還有兩個方法

  • Promise.any
  • Promise.allSettled

Promise.any 只要有任何一個成功,就必定會執行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const myName1 = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray1');
}, 300);
})
}

const myName2 = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
reject('Ray2');
}, 300);
})
}
Promise.any([myName1(), myName2()]).then(res => console.log(res))

最後 Promise.allSettled 只要傳入的 Promise 事件都完成或者失敗後就會回傳結果與狀態

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const myName1 = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray1');
}, 300);
})
}

const myName2 = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
reject('Ray2');
}, 300);
})
}
Promise.allSettled([myName1(), myName2()]).then(res => console.log(res))

Promise.allSettled 很適合用於確定所有 Promise 的執行狀態。

Promise 狀態

那麼 Promise 本身也有三種狀態

  • pending(擱置)
  • fulfilled(完成、實現)
  • rejected(拒絕)

如果你有針對前面的程式碼稍微嘗試運行過的話,基本上你都會看到上面任一種的提示訊息而這也是 Promise 的狀態。

Promise.prototype.then 特別之處

其實 Promise.prototype.then 是一個很特別的方法,為什麼這樣說呢?在前面範例中,我們知道 then 代表著 Promise 成功,而 catch 則是失敗,因此一個 Promise 就會寫成這樣:

1
2
3
4
5
6
7
8
9
10
11
const myName = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray');
}, 300);
})
}

myName()
.then((res) => console.log('成功:'+ res))
.catch((error) => console.log('失敗:' + error));

但是 Promise.prototype.then 是可以寫成這樣的:

1
2
3
4
5
6
7
8
9
10
const myName = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray');
}, 300);
})
}

myName()
.then((res) => console.log('成功:'+ res), (error) => console.log('失敗:' + error))

而這兩者都是屬於被允許的範圍內,但通常來講我們還是會採用前者的方式 then & catch,畢竟錯誤訊息與成功訊息都放在同一個 then 內是格外的難閱讀,更不用說當邏輯越來越複雜時。

另外 then 是可以串接的,因此如果希望依照順序執行的話,除了使用 Promise.all 之外也可以透過 return 持續將結果往下一個 then 傳遞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const myName1 = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray1');
}, 300);
})
}

const myName2 = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray2');
}, 300);
})
}
myName1().then((res) => {
console.log(res); // Ray1
return myName2()
}).then((res) => {
console.log(res); // Ray2
})

ES7 Async/Await

那麼前面的 Promise 語法幫助了我們遠離早期開發的 Callback Hell 問題

Callback Hell

讓我們可以從 Callback Hell 轉換為串聯的方式,但是實際開發來講,還是有可能會讓 Callback Hell 問題發生,因此 ES7 出現了一個新的語法,也就是 Async/Await,可以刊稱真正的 Callback Hell 救贖之光。

為什麼這樣說呢?我們都知道一個 Promise 都必須搭配 then 串接成功結果:

1
2
3
4
5
6
7
8
9
10
11
const myName = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray');
}, 300);
})
}

myName()
.then((res) => console.log('成功:'+ res))
.catch((error) => console.log('失敗:' + error));

而 ES7 Async/Await 出現之後我們可以大幅的減少巢狀結構,只需要這樣寫即可

1
2
3
4
5
6
7
8
9
10
const myName = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray');
}, 300);
})
}

const res = await myName();
console.log(res); // Ray

早期瀏覽器是不能直接如上方這樣寫的,因為 await 語法必須包在一個函式內:

1
2
3
4
5
6
7
8
9
10
11
12
13
const myName = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Ray');
}, 300);
})
}

async function fn() {
const res = await myName();
console.log(res); // Ray
}
fn();

是後來瀏覽器才允許你可以直接在頂層使用 await

Async

那麼 async 語法有什麼特別的呢?簡單來講 async function 可以將原本的函式轉換成 Promise

1
2
3
4
5
6
7
async function fn() {
return setTimeout(() => {
console.log('Ray');
}, 2000);
}

console.log(fn()); // Ray

Promise

這邊要注意一件事情,async 等同使用 Promise.resolve

1
2
3
async function fn() {
return 'Ray'
}

其實是被隱含轉換成了以下:

1
2
3
function fn() {
return Promise.resolve('Ray');
}

Await

那麼 await 呢?其實 await 概念等同 then

1
2
3
async function fn() {
return await 'Ray'
}

底層轉換成了 then 的概念:

1
2
3
function fn() {
return Promise.resolve('Ray').then(() => undefined)
}

async/await 特性

那麼看完前面的一些介紹之後,其實你就可以知道 async/await 可以更是大幅的優化 Promise 寫法,因此原本的 Promise 寫法可以如下:

1
2
3
4
5
6
const myName = function() {
return new Promise(function (resolve, reject) {
resolve('Ray');
})
}
myName().then((res) => console.log(res))

但是透過 async/await 特性之後,你可以改寫成以下:

1
2
3
4
5
const myName = async () => 'Ray';
myName().then((res) => console.log(res));

// 下面這種寫法也可以
myName().then(console.log);

甚至是這種極致的寫法:

1
2
const myName = async () => 'Ray';
console.log(await myName());

try/catch

那麼最後是關於 async/await 常見的狀況,因為使用 async/await 語法後我們就無法取得錯誤訊息,因此若要捕抓錯誤訊息的話,就必須使用 try/catch,而使用的方式非常簡單

1
2
3
4
5
6
const myName = async () => 'Ray';
try {
console.log(await myName());
} catch(e) {
console.log(e);
}

透過這樣的方式,就可以補抓一些錯誤訊息了。

實戰相關

那麼在實戰上我們很常使用 axios or fetch 等 API 獲取遠端的資料,而這兩者也都是使用 Promise 製作的,因此你不但可以使用 then 串鏈之外,也可以使用 async/await 撰寫,這邊就不再多次說明了,就當作留給閱讀的人一個功課,可以試著去回顧自己之前寫的 axiosfetch 語法讓它更簡短更優化吧。

參考文獻

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ