# 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` 等方法,如下: ![](https://i.imgur.com/jBzd2rZ.png) 透過 `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 回傳的值 ![](https://i.imgur.com/iyfqKXY.png) 以下範例來說,執行完函式直接 reject('失敗'),最終也能取得 rejected 的狀態及值。 ```javascript function promise() { return new Promise((resolve, reject) => { reject('失敗'); }); } console.dir(promise()); ``` ![](https://i.imgur.com/y2fPCot.png) ## 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)