# Promise
[TOC]
## 1. Promise 是什麼?
Promise 就是承諾,如同字面上的意思,當先前承諾的工作完成後,就來通知我們**可以進行下一步囉**。
## 2. 為什麼需要 Promise
常見的非同步問題:
1. 回呼地獄
2. 寫法不一致
3. 無法同時執行(無法確保哪個時間點開始、結束)
而 Promise 本身就是用來改善 JavaScript 非同步的語法結構。
另外,在 HTML 5 的 Web API 標準中,Event Loop 新增了微任務序列(micro task queue),而 Promise 正是透過微任務序列來驅動它的;微任務序列的觸發時機,是在 Stack 清空時,JavaScript 引擎會先確認微任務序列有沒有東西,有的話就優先執行,直到清空後,才從 Queue 拿出新任務到 Stack 上。
[複習Event Loop](https://ithelp.ithome.com.tw/articles/10230871)
```javascript
function Promise() {
return new Promise(resolve => {
console.log('promise func')
resolve()
})
}
Promise()
.then(() => console.log('then 1'))
.then(() => setTimeout(() => console.log('setTimeout'), 0))
.then(() => console.log('then 2'))
// promise func
// then 1
// then 2 -> 微任務優先於 setTimeout
// setTimeout
```
總之,就是為了解決一個問題:**確保非同步函式完成後才執行某個函式。**
## 3. Promise 的結構和狀態
### 3-1. 結構
Promise 本身是一個建構函式,函式也是屬於物件的一種,因此可以附加其它屬性方法在上,透過 `console.dir` 的結果可以看到 Promise 可以直接使用 `all、race、resolve、reject` 等方法,如下:

透過 `new Promise()` 的方式建立的物件,可以使用其中的原型方法,其中就包含 `then、catch、finally`,這些方法則必須在新產生的物件下才能呼叫。
除此之外,Promise 建構函式建立同時,必須傳入一個函式作為參數(executor function),此函式的參數包含 `resolve, reject`,
這兩個方法分別代表成功與失敗的回傳結果,特別注意這兩個僅能回傳其中之一,回傳後表示此 Promise 事件結束。
```javascript
new Promise(function(resolve, reject) {
resolve(); // 把成功的結果當作参數傳出去
reject(); // 把失敗的結果當作参數傳出去
});
```
`resolve` 及 `reject` 的名稱可以自定義,但在開發上大多數開發者習慣維持此名稱。
### 3-2. 狀態
Promise 的關鍵在處理非同步的事件,而非同步的過程中也包含著不同的進度狀態,在 Promise 的執行過程中,可以看到以下狀態:
- pending:事件已經運行中,尚未取得結果
- resolved:事件已經執行完畢且成功操作,回傳 resolve 的結果(該承諾已經被實現 fulfilled)
- rejected:事件已經執行完畢但操作失敗,回傳 rejected 的結果
進入 fulfilled 或 rejected 就不會再改變,Promise 中會使用 `resolve` 或 `reject` 回傳結果,並在調用時使用 `then` 或 `catch` 取得值。
如果要判斷 Promise 是否完成,可依據 Promise 事件中的 `resolve` 及 `reject` 是否有被調用,以下範例來說在沒有調用兩個方法時,Promise 的結果則會停留在 pending。
```javascript
function promise() {
return new Promise((resolve, reject) => {
// 沒有其他東西,所以維持pending狀態
});
}
console.dir(promise());
```
在 Promise 的執行函式中,可以看到以下兩個屬性:
[[PromiseStatus]]: "pending" -> 表示目前的進度狀態
[[PromiseResult]]: undefined -> 表示 resolve 或 reject 回傳的值

以下範例來說,執行完函式直接 reject('失敗'),最終也能取得 rejected 的狀態及值。
```javascript
function promise() {
return new Promise((resolve, reject) => {
reject('失敗');
});
}
console.dir(promise());
```

## 4. 創造自己的 Promise
為了熟悉 Promise,最好還是自己寫過一次。
### 4-1. 建立 Promise
函式陳述式建立以後,直接透過 `return new Promise` 回傳並建立一個 Promise 物件,並且在內部加入一個執行函式且帶上 `resolve`、 `reject` 的參數,到這個階段就是常見的 Promise 結構,接下來再依據執行的結果來透過 `resolve`、`reject` 回傳值即可。
```javascript=
// 拿泡麵舉例
function promiseCook (foodName, time) {
return new Promise((resolve, reject) => {
if (time >= 3 && time <= 5) {
resolve(`${foodName}泡了${time}分鐘,好吃`)
} else {
reject(`${foodName}泡了${time}分鐘,難吃`)
}
})
}
```
在呼叫前 Promise 前回顧一下 Promise 會有三個狀態:
- Pending :尚未得到結果
- Resolved:事件已經執行完畢且成功操作,回傳 `resolve`的結果
- Rejected:事件已經執行完畢但操作失敗,回傳 `rejected` 的結果
Promise 的狀態一開始會是 `pending` , 一旦 `resolve()` 被使用,狀態就會轉變為 `fulfilled`,而 `reject()` 被使用,狀態就會被轉變為 `rejected`。
這裡需注意的地方為 Promise 只有結果會影響狀態,無法透過一般外力操作使其更改狀態,而狀態一旦從 `pending` 改變後,就無法改變了。
在 `.then(onFulfilled, onRejected)`中可帶入兩個回呼函式,兩者分別又可以帶入各自的參數:
- onFulfilled:執行成功的函式,所帶入參數表示 Promise 函式中 `resolve` 所帶入的值。
- onRejected:執行失敗的函式,帶入參數表示 Promise 函式中 `reject` 所帶入的值。
```javascript
const cookTime = parseInt(Math.random() * 7) // 隨機帶入分鐘
promiseCook('來一客' , cookTime)
.then((res) => {
console.log(res)
})
.catch((err) => {
console.log(err)
})
```
在大部分情況下,開發者習慣僅使用 `.then()` 來取得成功的結果,失敗的部分交由 `.catch()` 來處理,這兩種寫法差異很小。
### 4.2 鏈接
為了確保非同步完成後才執行另一個方法,過去都只能不斷的透過 callback 的方式來確保下一個方法正確執行。
Promise 另一個特點在於 `then`、`catch` 都可以使用鏈接的方式不斷的進行下一個任務。
當我們要進行確保 Promise 任務結束後在進行下一個任務時,就可以使用 `return` 的方式進入下一個 `then`
```javascript
promiseCook('來一客' , 3)
.then((res) => {
console.log(res)
return promiseCook('再來一客' , 4)
})
.then((res) => {
console.log(res)
return promiseCook('又來一客' , 40) // 這個階段會進入 catch
})
.then((res) => {
console.log(res)
return promiseCook('還來一客' , 5) // 由於上一個階段結果是 reject,所以此段不執行
})
.catch((err) => {
console.log(err)
})
// 來一客泡了3分鐘,好吃
// 再來一客泡了4分鐘,好吃
// 又來一客泡了40分鐘,難吃
```
`return` 也有以下特點:
方法不限於 promise 函式,任何表達式都可進行回傳。
如果是 Promise 函式,則會繼續遵循 `then` 及 `catch` 的運作,如果不是 Promise 函式,在下一個 `then` 則可以取得結果。
```javascript
promiseCook('來一客' , 3)
.then((res) => {
console.log(res)
return '吃飽了,謝謝招待'
})
.then((res) => {
console.log(res)
})
// 來一客泡了3分鐘,好吃
// 吃飽了,謝謝招待
```
## 5. Promise 方法
### 5-1. Promise.all
有時候我們可能會想一次泡多碗泡麵,這時候就需要使用`Promise.all()`,其背後操作則是使用陣列將多個 promise 函式打包,當全部執行完成後回傳陣列結果,而陣列的結果順序與一開始傳入的一樣。 但是一旦有 Promise 物件失敗,將回傳失敗那個物件回傳的結果,如果是全部失敗,則回傳第一個 Promise 物件的失敗結果,來當成整個最後的錯誤訊息。
```javascript
Promise.all([ promiseCook('來一客' , 3),
promiseCook('雙響砲' , 4),
promiseCook('滿漢大餐', 5)
])
.then((res) => {
console.log(res)
})
.catch((err) => {
console.log(err)
})
// ['來一客泡了3分鐘,好吃', '雙響砲泡了4分鐘,好吃', '滿漢大餐泡了5分鐘,好吃']
```
```javascript
Promise.all([ promiseCook('來一客' , 3),
promiseCook('雙響砲' , 4),
promiseCook('滿漢大餐' , 6)
])
.then((res) => {
console.log(res)
})
.catch((err) => {
console.log(err)
})
// 滿漢大餐泡了6分鐘,難吃
```
這個方法很適合用在多支 API 要一起執行,並確保全部完成後才進行其他工作時。
### 5-2. Promise.race
`race` 和 `all` 不同的是只要有一個 Promise 物件回傳結果,不論成功或失敗,都會結束該次 `Promise.race()` 呼叫。
```javascript
Promise.race([ promiseCook('來一客' , 5),
promiseCook('雙響砲' , 4),
promiseCook('滿漢大餐' , 6)
])
.then((res) => {
console.log(res)
})
.catch((err) => {
console.log(err)
})
// 來一客泡了5分鐘,好吃
// 這邊也可以用setTimeout去模擬不同的時間,不然就是第一個會先執行完
```
這個方法可以用在站點不穩定,同時發送多支同行為 API 確保可行性使用,但實作中使用率並不高。
### 5-3. Promise.reject, Promise.resolve
直接定義Promise物件的狀態是reject或者resolve。(不知道要幹嘛用...)
範例如下:
```javascript
let result = Promise.resolve('result');
result.then(res => {
console.log('resolved', res); // 成功部分可以正確接收結果
}, err => {
console.log('rejected', err); // 失敗部分不會取得結果
});
```
改為 Promise.reject 產生 Promise 物件,此物件必定呈現 rejected 的結果。
```javascript
let result = Promise.reject('result');
result.then(res => {
console.log('resolved', res);
}, err => {
console.log('rejected', err); // 只有此段會出現結果
});
// rejected result
```
注意:Promise.reject、Promise.resolve 是直接定義結果,無論傳入的是否為 Promise 物件。
## 6. 其他
### 在做作品碰到的問題
因為API一次只會吐一頁的資料,本來想用Promise.all解決,
但是沒辦法解決頁面不照順序回傳的問題(每頁回傳時間不一定),後來助教建議`async/await`方式撰寫,就留到該主題再補充。
```javascript
getAllOrders () {
this.allOrders = []
this.revenue = 0
this.ordersNum = 0
status.isLoading = true
const axiosArray = []
for (let i = 1; i <= this.pagination.total_pages; i++) {
axiosArray.push(axios.get(url))
}
Promise.all(axiosArray).then((res) => {
for (let i = 1; i <= this.pagination.total_pages; i++) {
this.allOrders.push(...res[i - 1].data.orders)
res[i - 1].data.orders.forEach((item) => {
this.revenue += item.total
this.ordersNum += 1
})
status.isLoading = false
}
// console.log(this.allOrders)
this.getAllOrdersData()
})
.catch((err) => {
console.log(err)
})
}
```
## 參考資料
- [卡斯伯Blog](https://www.casper.tw/development/2020/02/16/all-new-promise/)
- [Summer。桑莫。夏天](https://www.cythilya.tw/2018/10/31/promise/)
- [是 Ray 不是 Array](https://israynotarray.com/javascript/20211128/2950137358/)
- [五倍紅寶石](https://5xruby.tw/posts/promise)
- [前端三十 - 成為更好的前端工程師系列 第 11 篇](https://ithelp.ithome.com.tw/articles/10221800)