---
tags: JavaScript, 六角筆記王
title: 主角還沒上場,配角怎麼偷跑了? 用 Promise 來改善吧!
---
# 主角還沒上場,配角怎麼偷跑了? 用 Promise 來改善吧!
Promise 是 ES6 所新增的建構函式,常用來處理同步與非同步的問題,也增強了程式碼的可讀性。而 JS 的執行順序是由上到下,若遇到事件則為進入事件佇列( Event queue )中,就是非同步的問題,像是 :
- AJAX 行為
- 監聽事件(click、change、…)
- `setTimeout( () => { } )`
## Promise 與 AJAX
我們可以用原生 fetch 語法或是直接使用第三方套件的 Axios 來做 AJAX 行為。
這邊提供一個資料測試網址([點我](https://randomuser.me/api)),它可以取得一些 JSON 格式的假資料。
### axios
以下是 axios 套件寫法 :
```javascript
// 利用 axios 的 get 方法取得資料
// 可以接收資料後, return 資料出來傳入下一個 then 使用
var url = 'https://randomuser.me/api'
axios.get(url).then(res=>{
console.log(res)
}).catch(err =>{
console.log(err)
})
```
### fetch
利用 fetch 原生語法來進行 :
```javascript
var url = 'https://randomuser.me/api'
fetch(url)
.then(response => {
// 必須從 response中 return response.json()給下一個 then
// response 為被鎖定的名為的物件,ReadableStream,其中一個方法會回傳 JSON格式
// 但是網址請求已被使用中,必須使用下一個 then 再度請求後回傳
return response.json();
})
.then(jsonData => {
console.log(jsonData);
})
```
### XMLHttpRequest
```javascript
var url = 'https://randomuser.me/api'
function get(url){
return new Promise((resolve, reject)=>{
var req = new XMLHttpRequest()
req.open('GET', url)
req.onload = function (){
if(req.status = 200){
// 成功時
resolve(req.response)
console.log(req.response)
} else {
// 失敗時
}
}
req.send()
})
}
get(url).then((res)=>{
console.log('get', res)
}).catch((err)=>{
console.log(err)
})
```
## Promise 與事件佇列
通常我們會使用一些監聽事件等待使用者去觸發,或是用一個計時器,例如這樣 :
### 一般事件佇列
```javascript
// setTimeout秒數就算設定為0,也會是最後出現
function fn(){
setTimeout( () => {
console.log('等待出現')
}, 0)
alert('使用者點擊')
}
var btn = document.querySelector('.btn')
btn.addEventListener('click', fn)
```
但如果目標是讓每經過一秒顯示一次呢?
```javascript
// 這樣會變成 1 秒後一起出現
// 而非每秒出現一次
// 當然也可以設計成 1000 > 2000 > 3000,但這不是我們要的
setTimeout( () => {
console.log('等待出現')
}, 1000)
setTimeout( () => {
console.log('等待出現')
}, 1000)
```
### 透過 Promise 來操作事件佇列
像是上述的例子,可以透過 Promise 來處理問題。
以下透過 Promise 來操作事件佇列 :
```javascript
function activeFn(time){
return new Promise((resolve, reject)=>{
setTimeout( () => {
resolve(`${time}毫秒後自動顯示`)
}, time)
})
}
activeFn(1000).then(res=>{
// 顯示第一次
console.log(res)
return activeFn(1000)
}).then(res=>{
// 顯示第二次
console.log(res)
})
```
## Promise 狀態
```graphviz
digraph graphname {
P [label="Promise" color=Block, fontcolor=Block, fontsize=24, ]
T [label=".then()" color=Blue, fontcolor=ForestGreen, fontsize=24, shape=box]
C [label=".catch()" color=Blue, fontcolor=Red, fontsize=24, shape=box]
P->T [label=" fulfilled ", fontcolor=block, ]
P->C [label=" rejected", fontcolor=block]
}
```
Promise會有三種狀態,分別是 :
| 狀態名稱 | 意義 |
| -------- | -------- |
| Pending | 待機 / 未確認,以 AJAX 行為來說,就是傳入網址等待取回資料 |
| Fulfilled | 已實現狀態,以 AJAX 行為來說,就是取值成功時 |
| Rejected | 已否決狀態,以 AJAX 行為來說,就是取值失敗時 |
以上一次只會有一種狀態,並在實際使用時分別用不同的關鍵字取用。
- 若進入 fulfilled
- 使用 then 接收上一個 return 的值
- 使用函式將值作為參數傳入 `.then((參數名稱)=>{console.log(參數)})`
- 若進入 rejected
- 使用 catch 接收錯誤時會回報的值
- 使用函式將值作為參數傳入 `.catch((參數名稱)=>{console.log(參數)})`
## Promise 建立
- Promise 是一個內建的函式 `(typeof Promise) // "function"`
- 可以使用 new 運算子轉為物件,但它需要傳入一個 function 才能運作
- 根據使用者認為該
- 情況正確使用 `resoleve()` 回傳資料
- 若不正確是用 `reject()` 回傳失敗訊息
- resolve 與 reject 可自定義參數名稱,不過為了避免混淆,還是習慣使用預設名稱
```javascript=
function promiseFn(num){
return new Promise((resolve, reject)=>{
// 非同步行為
setTimeout(()=>{
if(num){
resolve('成功!')
} else {
reject('失敗!')
}
}, 10)
})
}
promiseFn(1).then((res)=>{
console.log(res)
}).catch((res)=>{
console.log(res)
})
```
## Promise 回傳資料與串接
```javascript=
function promiseFn(num){
return new Promise((resolve, reject)=>{
// 非同步行為
setTimeout(()=>{
if(num){
resolve('成功!')
} else {
reject('失敗!')
}
}, 10)
})
}
promiseFn(1).then((res)=>{
console.log(res)
return promiseFn(2)
// 這個回傳的值會給下一個 then
// 若值為假值 promiseFn(0),會直接跳至 catch
}).then((res)=>{
console.log(res)
}).catch((res)=>{
console.log(res)
return promiseFn(4)
// 這個回傳的值會給下一個 then
}).then((res)=>{
console.log(res)
})
// 直接使用 then傳入兩個函式 then(函式 1, 函式 2),
// 前者接收 resolve狀態,後者 reject狀態
promiseFn(1).then(
(res)=>{
console.log(res, 'success')
return promiseFn(0) // 會傳給下一個 then 接收
},
(rej)=>{
console.log(rej, 'fail')
return promiseFn(1) // 會傳給下一個 then 接收
}).then(
(res)=>{
console.log('成功!')
},
(rej)=>{
console.log('失敗!')
})
```
## Promise 常用資料回傳方法
- Promise.all
- 等待全部執行完畢,並回傳所有結果
- 途中只要有狀態為 reject,會直接回傳 catch 結果
- Promise.race
- 只會回傳第一個執行完畢的函式並根據狀態回傳結果
- 只會回傳一組資料
### Promise.all
```javascript
function promiseFn(num, time){
return new Promise((resolve, reject)=>{
// 非同步行為
setTimeout(()=>{
if(num){
resolve('成功!')
} else {
reject('失敗!')
}
}, time)
})
}
promiseFn(1, 2000).then((res)=>{
console.log(res)
})
// 只要途中有一個執行失敗的話,就會進入 reject狀態並回傳結果,不會再往下執行
Promise.all([
promiseFn(1, 1000),
promiseFn(0, 2000),
promiseFn(1, 3000)
]).then(res=>{
console.log(res[0], res[1], res[2])
})
```
### Promise.race
```javascript
function promiseFn(num, time){
return new Promise((resolve, reject)=>{
// 非同步行為
setTimeout(()=>{
if(num){
resolve('成功!')
} else {
reject('失敗!')
}
}, time)
})
}
promiseFn(1, 2000).then((res)=>{
console.log(res)
}).catch((res)=>{
console.log(res)
})
// 只取第一個先跑完的結果,再根據狀態回傳
Promise.race([
promiseFn(1, 1000),
promiseFn(1, 500),
promiseFn(1, 3000)
]).then(res=>{
console.log(res)
}).catch((res)=>{
console.log(res)
})
```
## 事件佇列與一般結果
若想在事件佇列執行完畢後顯示其他結果,該怎麼做?
```javascript
function activeFn(time){
setTimeout( () => {
console.log(`${time}毫秒後自動顯示`)
}, time)
}
function activeFn2(){
console.log('最後顯示')
}
activeFn(3000)
activeFn2()
// "最後顯示"
// '1000毫秒後自動顯示"
```
### 利用 Promise 修正結果
```javascript
function activeFn(time){
return new Promise((resolve, reject)=>{
setTimeout( () => {
resolve(`${time}毫秒後自動顯示`)
}, time)
})
}
function activeFn2(){
return new Promise((resolve, reject)=>{
resolve(`最後顯示`)
})
}
// 執行函式 > 接收結果,return 函式執行的結果 > 接收結果
activeFn(3000).then(res=>{
// 顯示上一個函式執行的結果
console.log(res)
// return 另一個函式執行的結果
return activeFn2()
}).then(res=>{
// 顯示上一個函式執行的結果
console.log(res)
})
// "3000毫秒後自動顯示"
// "最後顯示"
```
## 參考來源
> 1. 六角學院 - 核心篇
> 2. [ OXXO.STUDIO - JavaScript 同步延遲 ( Promise + setTimeout )](https://www.oxxostudio.tw/articles/201706/javascript-promise-settimeout.html)
<!-- {%hackmd S1DMFioCO %} -->