[JS] Promise
TL;DR
Promise 用 法及範例
部份圖片來源為六角學院及卡斯柏'Blog。
參考資料
相關連結
- Promise | MDN
Promise的建立
Promise主要是為了解決非同步串接時,以往的寫法會導致過於巢狀的問題。
Promise本身是一個建構函式。透過new Promise()建立一個物件後,該物件就可以使用Promise的原型方法如:then
,catch
,finally
等等。
在建立一個Promise instance的時候,需要同時傳入一個函式做為參數,該函式參數又包含兩個參數分別代表onFullfilled
及onRejection
。一般的開發者習慣將這兩個參數命名為resolve
及reject
,但是實際上名稱可以自訂義。
以下是一個簡單的Promise instance建立範例:
new Promise(function(resolve, reject) {
resolve(); // 正確完成的回傳方法
reject(); // 失敗的回傳方法
});
Promise的狀態
Promise目的是為了處理非同步事件,過程中會有不同的狀態,包含:pending
,resolve
,reject
。
pending
:事件已經運行中,尚未取得結果resolved
:事件已經執行完畢且成功操作,回傳 resolve 的結果(該承諾已經被實現 fulfilled)rejected
:事件已經執行完畢但操作失敗,回傳 rejected 的結果
一個Promise
只會執行resolve
或是reject
其中一個,並且也只會執行一次!
在還沒有執行resove
或是reject
之前,Promise
的狀態會顯示為pending
,並且等待調用resolve
或reject
。
如以下範例,使用new
建構出一個Promise instance
後,因為並沒有調用resolve
或reject
,所以status
會顯示pending
:
new Promise((resolve, reject) => {});
一旦Promise執行完畢後,就會從pending狀態轉變成reject或是resolve其中一個。
-
reject
:new Promise((resolve, reject) => {reject('失敗');});
Rejected的時候需要使用catch去處理,因為我們沒有使用catch,所以這邊才會顯示紅色錯誤資訊。
-
resolve
:new Promise((resolve, reject) => {resolve('成功');});
使用函式陳述式建立Promise
一般我們在建立Promise的時候會透過函式陳述式
或函式表達式
來回傳一個Promise instance
出來:
函示陳述式
:
function promise () {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('成功');
}, 300);
})
}
函示表達式
:
const promise = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('成功');
}, 300);
})
}
以上兩種只有hosting有差別而已,在使用上沒有太大的不同。
Promise的使用及串接
我們可以透過.then跟.catch去取得Promise所回傳的結果:
-
建立
const promise = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('成功');
}, 300);
})
} -
接收回傳值
promise()
.then((res)=>console.log('Promise成功resolve:'+res))
.catch((error)=>console.log('Promise失敗reject:'+error));這三行程式碼只是拆分開來並且縮排,方便閱讀。實際上是串接在一起的。所以
.then
後方不可加入分號(;),否則會有語法上的錯誤。 -
執行結果如下:
因為我們只有撰寫resolve所以永遠只會跑到.then中成功的結果
then? catch?
什麼時候會跑then
,什麼時候又會跑catch
?取決於我們在建立Promise
的時候,調用到的是resolve
或是reject
。
如果今天調用resolve
,則後續串接會執行.then
內的內容。相對的,調用reject
則後續會執行.catch
的內容。
我們稍微調整一下函式陳述式內的內容:
const promise = function (num) {
return new Promise((resolve, reject) => {
num ? resolve(`${num}, 成功`) : reject('失敗');
});
}
使用三元運算子,如果我們所傳入的為truthy
,則會調用resolve
。反之,如果為falsy
,則會調用reject
。這樣我們就可以手動決定要調用的對象。
傳入1
的時候,會調用resolve
。此時這個Promise
的狀態會由pending
轉變為resolved
,並且會執行後續.then
內的內容。
傳入0
的時候,會調用reject
。此時Promise
狀態由pending
轉變為rejected
,並執行後續.catch
內容。
需注意,平常在使用Promise的時候,會調用到
resolve
或是reject
並不是由我們可以決定的
串接多個Promise
如果我們今天希望在第一個非同步事件結束之後,才能繼續執行第二個非同步事件。在以往的方法中,會變成巢狀的callback function hell。
延伸上述範例,我們可以改寫如下:
promise(1)
.then(success => {
console.log(success);
return promise(2);
})
.then(success => {
console.log(success);
return promise(0); // 這個階段會進入 catch
})
.then(success => { // 由於上一個階段結果是 reject,所以此段不執行
console.log(success);
return promise(3);
})
.catch(fail => {
console.log(`進到catch : ${fail}`);
})
Line:1 的Promise(1)
會調用resolve
,所以後續的.then
執行,並將resolve
內的值帶到success變數上印出。
Line:4 接著執行promise(2)
並回傳出第二個Promise instance,因為Promise(2)
的2
會帶入promise
內callback function的num參數。又因為2
是turthy
,所以三元運算子會調用resolve(`${num},成功`)
的內容,並且後續 Line:6 的.then
會執行內容。
Line:8 的promise(0)
會調用reject('失敗')
,所以 Line:10 ~ Line:13 的內容不會被執行,因為reject
所對應會執行的是.catch
片段,會直接忽略接下來所串接的.then內容,直到找到第一個.catch(也就是 Line:14 )。執行結果fail參數會帶入reject
所拋出的內容:失敗。
利用這種方法,就可以在每次進入.then
或是.catch
之後,再次回傳一個Promise instance
,並使用後續程式碼的.then
以及.catch
去判斷邏輯。這樣就不會導致callback hell,可以讓程式碼得以維護。
如果有需要在非同步事件操作失敗後,再進行一個非同步事件,也可以再次串接return promise(foo)
。就會從該函式後根據.then
跟.catch
再次進入邏輯判斷。
reject
內容其實可以由.then
接收其實.then
內是可以帶入兩個callback function的。第一組接收resolve的結果,第二組接收reject的。
撰寫方法如下:
// promise.then(onFulfilled, onRejected);
// 前者為 resolve callback,後者則為 reject
promise()
.then((success) => {
console.log(success);
}, (fail) => {
console.log(fail);
})
雖然有這種寫法,但是開發者仍然比較常使用catch去處理失敗的狀況,閱讀起來也比較舒適。
Promise靜態方法
我們在new Promise(()=>{})
建立一個Promise instance之後,就可以使用他的.then
以及.catch
等等的原型方法了。
但是Promise本身也有提供靜態的方法(即不需使用new就可以使用的方法),讓我們可以更簡單達到我們需要的效果。
主要會使用到的有Promise.all
,Promise.race
,Promise.any
三個。我們也可以透過console.dir(Promise)
去查看這些方法。
Promise.all
同時去戳多支API,希望等到全部的API都完成後再執行接下來的程式碼,就可以使用Promise.all
。使用方法如下:
const promise1 = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Promise1onFulfilled');
}, 2000);
})
}
const promise2 = function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
resolve('Promise2onFulfilled');
}, 5000);
})
}
Promise.all([promise1(), promise2()])
.then((res) => {
console.log(res); //["Promise1onFulfilled", "Promise2onFulfilled"]
})
Promise.race
用法如下,先 稍微調整一下兩個函式內容,增加一個duration可以手動調整非同步的完成時間,並且設定預設值分別為5秒及15秒:
const promise1 = function(num,duration=5000) {
return new Promise(function(resolve, reject) {
setTimeout(() => {
num ? resolve(`Promise1onFulfilled,num=${num}`) : reject(`Promise1onRejection,num=${num}`);
}, duration);
})
}
const promise2 = function(num,duration=15000) {
return new Promise(function(resolve, reject) {
setTimeout(() => {
num ? resolve(`Promise2onFulfilled,num=${num}`) : reject(`Promise2onRejection,num=${num}`);
}, duration);
})
}
Promise.race([promise1(0), promise2(1)])
.then((res) => {
console.log('then'+res);
})
.catch((fail) => {
console.log('catch'+fail);
})
Promise.race
會比較兩個promise,先完成狀態的就會執行,另一個則完全不會執行。
得到的結果是來自於哪個promise,與onFullfilled
或onRejection
完全無關。只取決於是哪一個promise先結束pending的狀態。
Promise.any
const promise1 = function(num,duration=5000) {
return new Promise(function(resolve, reject) {
setTimeout(() => {
num ? resolve(`Promise1onFulfilled,num=${num}`) : reject(`Promise1onRejection,num=${num}`);
}, duration);
})
}
const promise2 = function(num,duration=15000) {
return new Promise(function(resolve, reject) {
setTimeout(() => {
num ? resolve(`Promise2onFulfilled,num=${num}`) : reject(`Promise2onRejection,num=${num}`);
}, duration);
})
}
Promise.any([promise1(0), promise2(1)])
.then((res) => {
console.log('then'+res);
})
.catch((fail) => {
console.log('catch'+fail);
})
使用Promise改寫XMLHttpRequest
這部分還沒有時間自己吸收修改成自己的版本,只是先做個紀錄方便自己參照。
原始文章來自於卡斯柏'Blog,等有空的時候會再自己吸收修改內容...
請老師見諒m(_ _)m
Promise 很大一部份是用來處理 Ajax 行為,此段透過改寫的形式了解使用 Promise 及傳統的寫法有哪些差異。
傳統上,需透過 XMLHttpRequest 建構式來產生可進行遠端請求的物件,並且依序定義方法(GET)及狀態(onload)並送出請求(send),取得結果後的其它行為則需要撰寫在 onload 內,程式碼結構如下:
var url = 'https://jsonplaceholder.typicode.com/todos/1';
// 定義 Http request
var req = new XMLHttpRequest();
// 定義方法
req.open('GET', url);
// 當請求完成,則進行函式的結果
req.onload = function() {
if (req.status == 200) {
// 成功直接列出結果
console.log(req.response);
} else {
// 失敗的部分
}
};
// 送出請求
req.send();
接下來將以上的行為封裝至 get 函式內,此函式包含 Promise 及上述的 XMLHttpRequest 行為,運用時只要直接使用 get(url)...,接下來的運用方式則是符合 Promise 的結構,重複運用的情況下程式碼可以大幅提高易讀性。
function get(url) {
return new Promise((resolve, reject)=> {
// 定義 Http request
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
if (req.status == 200) {
// 使用 resolve 回傳成功的結果,也可以在此直接轉換成 JSON 格式
resolve(JSON.parse(req.response));
} else {
// 使用 reject 自訂失敗的結果
reject(new Error(req))
}
};
req.send();
});
}
// 往後的 HTTP 直接就能透過 get 函式取得
get('https://jsonplaceholder.typicode.com/todos/1')
.then((res) => {
console.log(res);
})
.catch((res) => {
console.error(res)
})
axios與Promise
axios | Github
Axios是一個Promise based的套件。他的回傳值會經過封裝,需要透過點記法才能取到我們需要的內容。 以下提供一個API,可以取得一個隨機使用者的假資料:RANDOM USER GENERATOR
axios.get('https://randomuser.me/api')
.then(res=>{
console.log('成功',res)
})
.catch(err=>{
console.log('失敗',err)
});
成功取得資料會得到一個封裝過的物件:
將url
修改,因為無此路由所以axios會判斷調用resolve
,由我們撰寫的catch去處理失敗狀況:
axios.get('https://randomuser.me/api/gg')
.then(res=>{
console.log('成功',res)
})
.catch(err=>{
console.log('失敗',err)
});
取得data
因為axios會將回傳內容給封裝,所以我們要取得實際需要的內容要撰寫如下:
axios.get('https://randomuser.me/api')
.then(res=>{
console.log('成功',res.data.results)
})
.catch(err=>{
console.log('失敗',err.response)
});