前端基礎
-前端重要觀念 Promise - 為甚麼要有 Promise?如何使用?
到底甚麼是 Promise?
用一句話說,Promise 是一個對 還未有結果的事物 的一個替身,且他不是前端才有的東西。
那為甚麼要對未有結果的事物做一個替身呢?
這篇文有提到,我們不知道非同步的程式什麼時候會回傳東西過來,這種不確定性讓我們沒辦法對他有很好的操作,例如回傳來失敗我們也無法知道,但 Promise 就可以很好的解決這個問題。
同步 vs 非同步
簡單複習一下,程式碼有分同步和非同步:
- 同步:一件事情做完接著做另一件事情,比如先刷牙在洗澡。
- 非同步:一次做多件事情,比如同時刷牙和洗澡。
我們無法確定非同步什麼時候會結束,就好比我們不確定會先刷完牙還是洗完澡,只有真的結束的時候,才會知道。
且回調函數會有回調地獄的問題,並且回調函數也沒有一個規範,每個人的寫法都不一樣,造成使用和閱讀上的困難。 為了解決這些問題,有了 Promise 的誕生。
大陸翻譯叫做期約,台灣大多數都直接說 Promise
如何創建 Promise
要使用 Promise 的方法很簡單 👇
let p = new Promise(() => {})
console.log(p) // Promise {<pending>}
因為 Promise 的特性,我們必須要傳一個函數當作參數給他,不然會報錯,這個等等會提到。
💡 複習:根據我們對 class、建構式的理解,可以知道這個 p 是一個 Promise 的實例,而且應該會有自己的屬性和函式
Promise 狀態
眼尖的你有發現前面 console.log(p)
後有一個 {<pending>}
嗎?這是 Promise 的狀態,那為甚麼要有這個狀態呢?
還記得 Promise 是對不存在結果的一個替身嗎,這個狀態就是為了形容這個結果的!
Promise 有三種狀態:
- pending (待定):還未回傳結果
- fulfilled (兌現):回傳成功,又被稱為 resolved (解決)
- rejected (拒絕):回傳失敗
這樣我們就可以根據這三種狀態來去處理資料。
Promise 處理狀態
那我們要如何根據這三種狀態來去處理 Promise 呢?其實 promise 的狀態是私有的,我們沒辦法透過以下這種方式來去修改:
p.PromiseState = 'pending'
這也很好理解,因為如果可以這樣亂改那整個程式都要亂掉了,所以前面才說要傳入一個函數當作參數,因為我們要像下面這樣,在 Promise 的內部來去操作 Promise 的狀態:
function createPromise(success) {
return new Promise((resolve, reject) => {
// 處理資料...
if (success) {
resolve('資料回傳成功!')
}
else {
reject('資料回傳失敗!')
}
})
}
接著我們先來自己操作 success 來試試看
let p1 = createPromise(true);
console.log(p1); // Promise {<fulfilled>: '資料回傳成功!'}
let p2 = createPromise(false);
console.log(p2) // Promise {<rejected>: '資料回傳失敗!'}
上面的 resolve()
和 reject()
就是告訴 promise 現在是成功還是失敗,如果 success 則使用 resolve()
代表成功,反之則使用 reject()
代表失敗。有發現我們 console
出來後有顯示 fulfilled
和 rejected
嗎? 這就是成功和失敗的狀態,而 Promise 的狀態只要一改變就沒辦法再變回去 pending 了
Promise 如何處理回傳的資料
那我們要怎麼使用取得非同步傳回來的資料呢?只要使用 .then()
就可以了
p1 = createPromise(true)
.then(data => console.log(data));
// '資料回傳成功!'
.then( )
裡面要放一個函數 而這個函數的參數就是傳給 resolve()
裡的參數。
那這個 .then
到底是甚麼意思? 還記得上次回調函數的方式嗎,取得第一筆資料,然後再用第一筆資料的地址抓第二筆資料。
這個然後就是 .then()
就像是"我先吃早餐,然後上班的"這個然後,這也是 promise 厲害的地方,他可以等 resolve()
結束,然後接著做其他事情。
如果 Promise 回傳資料失敗怎麼辦? .catch()
前面的 .then()
是 promise 成功 (fullfilled) 才會調用,這也很好理解,都失敗了還有甚麼然後勒 ,所以我們要用 .catch()
來處理失敗的狀況,catch
就是抓住錯誤的意思,比如以下程式碼:
p2 = createPromise(false)
.then(data => console.log(data))
.catch(err => console.log(err));
// '資料回傳失敗'
❗注意,只要發生錯誤,就會直接執行.catch()
而跳過所有的.then()
連鎖 promise
有時候我們 promise 成功後想要執行東西,執行成功後又想要執行其他東西,就可以不斷的 .then()
來做,例如:
let p = new Promise((resolve, reject) => {
console.log('first');
resolve();
});
p.then(() => console.log('second'))
.then(() => console.log('third'))
.then(() => console.log('fourth'));
// first
// second
// third
// fourth
不過這沒什麼太大的意義,因為直接寫同步的函數也可以做到:
(() => console.log('first'))();
(() => console.log('second'))();
(() => console.log('third'))();
(() => console.log('fourth'))();
所以連鎖 promise
厲害的地方是用在多個非同步程式碼,在 .then()
的地方回傳一個新的 promise
這樣就可以確保每個函數都會等待之前的 promise
完成。
我用以下程式碼做示範,為了方便,我們先寫一個製作 promise
的函數,並利用 setTimeout 模擬非同步。
💡setTimeout 會讓瀏覽器去計時,這個時候 JS 先執行其它內容,等瀏覽器計時完後告訴 JS,讓 JS 執行傳入的函數,所以 setTimeout 就是一個非同步的例子。
function createPromise(seconds, data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(data);
resolve();
}, seconds * 1000);
});
}
接著來用 .then 依序執行
let p1 = createPromise(1, 'p1');
p1.then(() => createPromise(1, 'p2'))
.then(() => createPromise(1, 'p3'))
.then(() => createPromise(1, 'p4'));
// p1 (1 秒後)
// p2 (2 秒後)
// p3 (3 秒後)
// p4 (4 秒後)
利用 Promise 改寫回調地獄
在這 你知道非同步嗎 貼文中,我們用回調函數示範了回調地獄,也就是
function getSomeData(url, callback) {
// 獲取 data
callback(yourData);
}
getSomeData(url1, (data1) => {
getSomeData(data1.url, (data2) => {
getSomeData(data2.url, (data3) => {
getSomeData(data3.url, (data4) => {
...
})
});
})
})
// other operation
我們用 Promise 來改寫:
function getSomeDataPromise(url) {
return new Promise((resolve, reject) => {
// 利用 url 獲取 yourData
if (success) {
resolve(yourData);
}
else {
reject('獲取錯誤')
}
})
}
getSomeDataPromise(url)
.then(data1 => {
console.log(data1);
return getSomeDataPromise(data1.url)
})
.then(data2 => {
console.log(data2);
return getSomeDataPromise(data2.url)
})
.then(data3 => {
console.log(data3);
return getSomeDataPromise(data3.url)
})
.catch(err => {
console.log(err)
})
可以發現利用 Promise.then()
比直接使用 callback 簡潔的多。
Promise 的其它方法
除了 resolve()
和 reject()
以外,promise 還有其他好用的函數像是
- all()
- allSettled()
- race()
- finally()
和上次提到的 promise 連鎖不同,all()
和 race()
是利用合成的方式來處理 promise,我們直接看例子會比較清楚。
Promise.all()
all()
會將多個 promise 組合起來,等到全部的 promise 都完成執行後才會再執行all()
,用上次連鎖 promise 的例子 👇
function createPromise(seconds, data, success = true) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (success) {
resolve(data);
}
else {
reject('err');
}
}, seconds * 1000);
});
}
let p1 = createPromise(1, 'p1');
let p2 = createPromise(2, 'p2');
let p3 = createPromise(3, 'p3');
Promise.all([p1, p2, p3]).then(res => console.log(res))
// 3 秒後回傳 [p1, p2 ,p3]
因為用 Promise.all 回傳的資料順序和你傳入的順序相同的
所以這個方法適合用在同時執行多個 API ,且希望回傳的順序照是你預期的情況。
但若中間有失敗的 promise,就只會回傳失敗的訊息。
function createPromise(seconds, data, success = true) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (success) {
resolve(data);
}
else {
reject('err');
}
}, seconds * 1000);
});
}
let p1 = createPromise(1, 'p1');
let p2 = createPromise(2, 'p2', false);
let p3 = createPromise(3, 'p3');
Promise.all([p1, p2, p3])
.then((res) => console.log(res))
.catch((err) => console.log(err));
// 'err'
Promise.allSettled()
這是 ES11 新增的語法,是為了解決 Promise.all()
的缺點。
前面說 Promise.all()
失敗的時候只會返回失敗的訊息,但有時候我們可能成功失敗的資料都需要拿到,這時就可以使用 Promise.allSettled()
,他會返回陣列並包含所有的訊息,拿前面的相同例子來說:
let p1 = createPromise(1, 'p1');
let p2 = createPromise(2, 'p2', false);
let p3 = createPromise(3, 'p3');
Promise.allSettled([p1, p2, p3])
.then((res) => console.log(res))
.catch((err) => console.log(err));
// [
// {status: 'fulfilled', value: 'p1'},
// {status: 'rejected', reason: 'err'},
// {status: 'fulfilled', value: 'p3'}
// ]
Promise.race()
和 all()
相反 race()
是誰先執行完就回傳誰的結果
一樣來看例子
function createPromise(seconds, data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// console.log(data);
resolve(data);
}, seconds * 1000);
});
}
let p1 = createPromise(1, 'p1');
let p2 = createPromise(2, 'p2');
let p3 = createPromise(3, 'p3');
Promise.race([p1, p2, p3]).then(res => console.log(res))
// 1 秒後回傳 p1
因為 p1 先完成就直接回傳 p1 的結果,後面的就也不會執行,也就不會管後面是失敗還成功。
Promise.prototype.finally()
finally()
是指 promise 結束後會執行的函式,有時候我們希望不管 promise 是成功還失敗都執行一些東西,就可以用到 finally()
:
function createPromise(seconds, data, success = true) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (success) {
console.log(data);
resolve();
}
reject('err');
}, seconds * 1000);
});
}
let p1 = createPromise(1, 'p1');
p1.then(() => createPromise(1, 'p2'))
.then(() => createPromise(1, 'p3'))
.then(() => createPromise(1, 'p4'))
.catch((err) => console.log(err))
.finally(() => console.log('done'));
不管成功失敗,最後都會回傳 done。
Promise.then() 的第二個參數
其實 .then
最多可以傳入兩個函數當作參數 第一個是接收成功的資料,第二個接受失敗的資料。
和 .catch()
不同的是 .catch()
會跳過所有的 .then()
而 .then()
失敗之後還可以接其他的 .then()
繼續執行其它函數。
function onResolved(id) {
setTimeout(console.log, 0, id, 'resolved');
}
function onRejected(id) {
setTimeout(console.log, 0, id, 'rejected');
}
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));
p1.then(() => onResolved('p1'),
() => onRejected('p1'));
p2.then(() => onResolved('p2'),
() => onRejected('p2'));
// 3秒後
// p1 resolved
// p2 rejected
Promise 觀念加強 - 不能 return 賦值給外層變數
在我剛學 Promise 的時候,我以為可以在 Promise 裡面 return
結果給一個變數,用 fetch 串接 API 取得資料當例子:
const result = fetch('https://jsonplaceholder.typicode.com/posts/1')
.then((response) => response.json())
.then((json) => {
return json;
});
console.log(result) // 操作 result
// [object Promise]
// {} 👈 結果會是空物件
如果你不知道甚麼是 fetch,可以先參考這篇文章。
但這樣是不行的,因為就如同文章開頭所說 Promise 是一個對 還未有結果的事物 的一個替身,換句話說, Promise 代表了一個將來某一時刻才會知道結果的非同步操作。所以我們無法在直接將結果賦值給變數,因為 JS 執行到 fetch 那行程式碼時,我們仍不知道結果為何。
const result = fetch('https://jsonplaceholder.typicode.com/posts/1')
// 👆 我們不知道這時候的結果
.then((response) => response.json())
.then((json) => {
return json;
});
console.log(result) // 操作 result
所以當你在 Promise return 時時,你實際上返回的是一個 Promise
對象,而不是 Promise
內部 resolve
或 reject
的值。
這也是未來 async / await 誕生的一個原因,這部分我們放在下篇說明。
小結
這樣 promise 就差不多講完了,今天提到的部分有
- 甚麼是 Promise
- 如何創建 Promise
- Promise 狀態與處理
- Promise 的其它用法
- Promise 觀念加強
希望你對 promise 有更了解一點,我覺得把 promise 當作一個替身的想法有幫助理解的,你覺得呢?
那今天就這樣,下篇貼文見~!