--- 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 %} -->