跳至主要内容

[JS] Promise

TL;DR

Promise 用法及範例

部份圖片來源為六角學院及卡斯柏'Blog

參考資料

相關連結


Promise的建立

Promise主要是為了解決非同步串接時,以往的寫法會導致過於巢狀的問題。

Promise本身是一個建構函式。透過new Promise()建立一個物件後,該物件就可以使用Promise的原型方法如:then,catch,finally等等。

在建立一個Promise instance的時候,需要同時傳入一個函式做為參數,該函式參數又包含兩個參數分別代表onFullfilledonRejection。一般的開發者習慣將這兩個參數命名為resolvereject,但是實際上名稱可以自訂義。

以下是一個簡單的Promise instance建立範例:

new Promise(function(resolve, reject) { 
resolve(); // 正確完成的回傳方法
reject(); // 失敗的回傳方法
});

Promise的狀態

Promise目的是為了處理非同步事件,過程中會有不同的狀態,包含:pending,resolve,reject

  • pending:事件已經運行中,尚未取得結果
  • resolved:事件已經執行完畢且成功操作,回傳 resolve 的結果(該承諾已經被實現 fulfilled)
  • rejected:事件已經執行完畢但操作失敗,回傳 rejected 的結果

狀態示意圖(圖片來源為六角學院卡斯柏老師)

Promise的狀態-pending

一個Promise只會執行resolve或是reject其中一個,並且也只會執行一次!

在還沒有執行resove或是reject之前,Promise的狀態會顯示為pending,並且等待調用resolvereject

如以下範例,使用new建構出一個Promise instance後,因為並沒有調用resolvereject,所以status會顯示pending

new Promise((resolve, reject) => {});

status顯示為pending

一旦Promise執行完畢後,就會從pending狀態轉變成reject或是resolve其中一個。

  1. reject

    new Promise((resolve, reject) => {reject('失敗');});

    狀態轉為rejected

    Rejected的時候需要使用catch去處理,因為我們沒有使用catch,所以這邊才會顯示紅色錯誤資訊。

  2. resolve

    new Promise((resolve, reject) => {resolve('成功');});

    狀態轉為resolved

使用函式陳述式建立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。

Image

延伸上述範例,我們可以改寫如下:

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}`);
})

Promise的串接

Line:1Promise(1)會調用resolve,所以後續的.then執行,並將resolve內的值帶到success變數上印出。

Line:4 接著執行promise(2)並回傳出第二個Promise instance,因為Promise(2)2會帶入promisecallback functionnum參數。又因為2turthy,所以三元運算子會調用resolve(`${num},成功`)的內容,並且後續 Line:6.then會執行內容。

Line:8promise(0)會調用reject('失敗'),所以 Line:10 ~ Line:13 的內容不會被執行,因為reject所對應會執行的是.catch片段,會直接忽略接下來所串接的.then內容,直到找到第一個.catch(也就是 Line:14 )。執行結果fail參數會帶入reject所拋出的內容:失敗

利用這種方法,就可以在每次進入.then或是.catch之後,再次回傳一個Promise instance,並使用後續程式碼的.then以及.catch去判斷邏輯。這樣就不會導致callback hell,可以讓程式碼得以維護。

catch後還可以再接Promise

如果有需要在非同步事件操作失敗後,再進行一個非同步事件,也可以再次串接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原型內可使用的方法

但是Promise本身也有提供靜態的方法(即不需使用new就可以使用的方法),讓我們可以更簡單達到我們需要的效果。
主要會使用到的有Promise.all,Promise.race,Promise.any三個。我們也可以透過console.dir(Promise)去查看這些方法。

Image

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,與onFullfilledonRejection完全無關。只取決於是哪一個promise先結束pending的狀態

將兩個promise的時間設定相同,則順序會影響結果

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)
});

錯誤由catch處理

取得data

因為axios會將回傳內容給封裝,所以我們要取得實際需要的內容要撰寫如下:

axios.get('https://randomuser.me/api')
.then(res=>{
console.log('成功',res.data.results)
})
.catch(err=>{
console.log('失敗',err.response)
});

正確取得資料

修改成錯誤路由如下:

// 刻意修改錯誤路由
axios.get('https://randomuser.me/api/gg')
.then(res=>{
console.log('成功',res.data.results)
})
.catch(err=>{
console.log('失敗',err.response)
});

無法取得資料時錯誤處理內容在response內