今天會介紹常用來處理非同步程式的語法:Promise
。在ES6的Promise
語法出現之前,我們都依賴回傳函式來寫非同步程式,但Promise
的出現,令整個非同步程式的流程結構更易讀更易維護。
本文會解釋:
Promise.all
、Promise.race
)XMLhttprequest
Promise
與回傳函式一樣,同樣是用來處理非同步程式。最常用到非同步程式的情況,都是跟網絡連線有關(例如fetch
抓取遠端資料),或者跟排程有關(例如setTimeout
)。
不同的是,Promise
改善了非同步的語法結構,比起只用回傳函式,更易閱讀和維護。過往我們要確保非同步函式完成後才執行某個函式,我們會用到回傳函式的方法來完成,可是如果在該回傳函式裏再接一個回傳函式,形成多層的巢狀結構,就會造成回傳地獄(callback hell)的慘況。但用Promise
去寫,就會變得較易閱讀。
繼Promise
出現的是async/await,也是目前最新的寫法,它是把Promise
寫得更簡潔的語法糖,背後操作原理與Promise
是一樣。總括來說,Callback function、Promise
、async/await其實都是在解決同一個問題:確保非同步函式完成後才執行某個函式。
Promise
基本語法Promise
用處:
我們可以透過Promise
物件,得出一個非同步函式在完成後,最後得出的成功或失敗結果值。
Promise
是一個建構函式,我們要用new
來產生一個實體的Promise
物件:
//以下只用作解說Promise基本語法,很少會這樣用Promise
const p = new Promise(function(resolve,reject){ //傳入函式,帶有2個参數
//你想執行的非同步工作
setTimeout(function(){
resolve('成功!'); //工作成功,呼叫resolve函式,把結果透過参數傳出去
},1000)
})
在建立Promise物件時,需要傳入一個函式作為参數,該函式裏須帶有兩個参數,分別是resolve
和reject
函式。
resolve
函式:
pending
變成fulfilled
reject
函式:
pending
變成rejected
以上例子是假設Promise
所處理的工作結果是成功。但只是寫以上步驟的話,是不會抽取到成功的結果(即是resolve(...)
裏的東西)。我們之後還需要在該Promise
物件中使用then
方法,把函式resolve
中的参數(即是成功的結果)抽取出來:
const p = new Promise(function(resolve,reject){
//你想執行的非同步工作
setTimeout(function(){
resolve('成功!'); //工作成功,呼叫resolve函式,把結果透過参數傳出去
},1000)
})
//在Promise物件使用then方法
p.then(function(result){
//result 是任何你由上方 resolve(...) 傳入的東西
console.log(result) //成功!
})
失敗的結果,就可以用catch
方法來取得:
const p = new Promise(function(resolve,reject){
setTimeout(function(){
reject('失敗!');
},1000)
})
p.then(function(result){
console.log(result);
})
.catch(function(error){
console.log(error); //失敗!
})
注意:result
和error
這兩個名字是任你改的。同樣地,在Promise
物件裏的resolve
和reject
這兩個名字都是任你改,只是我們習慣會用resolve
和reject
。
then
也可以抽取失敗結果,但不建議then
方法除了可以抽取成功結果,也能抽取失敗結果。我們可以在then
方法同時放兩個回傳函式作為参數:
p.then(function(result){
console.log(result);
},function(error){
console.log(error); //失敗!
})
然而,在捕捉錯誤時,用catch
寫法比then
更好,因為在除錯時,我們可以集中在catch
之前的then
裏面尋找問題。同時也比較接近以前會用的try/catch
寫法。
// 不建議用then寫法
p.then(function(result) {
// success
}, function(error) {
// error
});
// 建議用catch寫法
p.then(function(result) {
// success
})
.catch(function(error) {
// error
});
上面有輕輕提及過Promise
可以有3種狀態,包括:pending
、fulfilled
、rejected
。
pending
:事件運作中,還未有結果resolved
:事件已完成,結果成功,回傳resolve(...)
裏的結果rejected
:事件已完成,結果失敗,回傳rejected(...)
裏的結果Promise
要麼回傳resolved
,要麼回傳rejected
,而且只會回傳一次。
例如我把以上例子改回成功結果,並且在不同位置查看一下Promise
的狀態:
const p = new Promise(function(resolve,reject){
setTimeout(function(){
resolve('成功!'); //呼叫resolve函式,會把Promoise狀態變成fulfilled
},1000)
})
p.then(function(result){
console.log(p) //Promise {<fulfilled>: "成功!"}
console.log(result)
})
//p是非同步,所以下面這行會先被執行,這時候p是pending狀態
console.log(p) //Promise {<pending>}
以上例子就是Promise
執行完setTimeout
函式後得出成功結果後,從pending
狀態變成fulfilled
狀態的例子。
為什麼最後一行會得出pending
,之後上面才得出fulfilled
? 因為setTimeout
的程式是非同步的,主程式會先跳過它,直接執行最後一行console.log(p)
,這時候setTimeOut
裏的工作並未被執行,所以Promise
就是pending
狀態。當setTimeout
的計時結束,主程式回去執行setTimeOut
裏的程式,並且結果是成功,所以Promise
就會由pending
變成fulfilled
。
下圖具體解釋了Promise
狀態的變化流程:
圖片來源:JAVASCRIPT.INFO
我們需要完成一個非同步函式後,接著執行下一個非同步函式。這時候我們就可以用鏈接的技巧。以下例子是把兩個數字相加,如果結果是整數就會回傳resolve
,負數就會回傳reject
。例子参考自這裏。
function add(a,b){
return new Promise( (resolve,reject) => {
window.setTimeout( () => {
let sum = a + b;
sum > 0 ? resolve(`${sum}, 成功`) : reject(`${sum}, 失敗`);
},1000);
})
}
add(10,10)
.then( success => {
console.log(success); //20, 成功
return add(5,10);
})
.then( success => {
console.log(success); //15, 成功
return add(3,-5);
})
.then( success => {
console.log(success);
return add(0,2) //沒有被執行
})
.catch( error => {
console.log(error); //-2, 失敗
})
當其中一個結果(add(3,-5)
)是失敗時,就會跳過下一個then
方法,直接跳到catch
方法。所以在以上例子中,add(0,2)
是不會被執行。所以要注意,這裏全部4個Promise
物件(add(10,10)、add(5,10)、add(3,-5)、add(0,2)),只要其中一個的結果是錯誤,就會跳到catch
方法。
在then
或catch
這些抽取成功或失敗結果值的方法裏,我們不止可以再回傳Promise
物件,也可回傳其他表達式。以下範例中,執行完check(2)
後,如果check(2)
的結果成功,我就回傳另一個函式multiply
:
function check(num1){
return new Promise( (resolve,reject) => {
window.setTimeout( () => {
num1 > 0 ? resolve(num1) : reject(`${num1},失敗`)
},1000);
})
}
function multiply(num2){
console.log(`${num2} * 10 = ${num2*10}`) //2 * 10 = 20
}
check(1)
.then( success => {
console.log(success); //1
return check(2)
})
.then( success => {
//回傳其他表達式
return multiply(success);
})
.catch( error => {
console.log(error);
})
Promise常用方法除了then
、catch
,還有:
Promise.all
:Promise
函式,並完成所有Promise
函式,最後把所有結果集合在一個陣列裏,並且回傳Promise.race
:Promise
函式,回傳最快完成的那個Promise
物件Promise.reject
、Promise.resolve
:Promise
物件的狀態是reject
或者resolve
Promise.all
以陣列形式傳入多個Promise
函式,一定會等到完成所有Promise
函式,才會回傳結果,而結果就是一個放有所有Promise
物的陣列。例如以下例子,我需要p1
和p2
都一併完成,才會執行下一個動作:
function add(num1,num2,delayTime){
return new Promise( (resolve,reject) => {
window.setTimeout( () => {
resolve(num1 + num2);
},delayTime)
})
}
const p1 = add(10,20,1000);
const p2 = add(60,70,3000);
Promise.all([p1,p2]).then( success => {
let [result1,result2] = success
console.log(result1,result2) //30,130
})
Promise.race
以陣列形式傳入多個Promise
函式,回傳最快完成的那個Promise
物件。
Promise.race([p1,p2]).then( success => {
console.log(success) //30
})
Promise.reject
、Promise.resolve
直接定義Promise
物件的狀態是reject
或者resolve
。範例如下:
const p1 = Promise.resolve('Hello World');
const p2 = Promise.reject('Error!');
Promise.all([p1,p2])
.then( success => {
console.log(success);
})
.catch( error => {
console.log(error); //Error!
})
Promise
改寫XMLHttpRequest
來處理AJAX重溫我們如何用XMLHttpRequest
來抓取遠端伺服器的資料:
const xhr = new XMLHttpRequest();
xhr.open('get','https://randomuser.me/api/',true);
xhr.send(null);
//當遠端資料已傳回來,就執行以下函式
xhr.onload = () => {
if(xhr.status === 200){
console.log(xhr.responseText);
}else{
console.log('請求失敗!')
}
}
以上範例以XMLHttpRequest
建構函式來產生XMLHttpRequest
實體物件,並向遠端發出資料請求。
如果用Promise
重寫的話,做法就是把XMLHttpRequest
的部分放在Promise
裏處理:
function getJSON(url){
return new Promise( (resolve,reject) => {
const xhr = new XMLHttpRequest();
xhr.open('get',url);
xhr.send(null);
xhr.onload = () => {
if(xhr.status === 200){
resolve(JSON.parse(xhr.responseText));
}else{
reject(new Error(xhr.statusText));
}
}
});
};
//把url當作参數
getJSON('https://randomuser.me/api/')
.then(JSONdata => console.log(JSONdata))
.catch(error => console.log(error))
這個做法提高了程式碼的可讀性。如果我們之後又要從不同遠端伺服器抓取資料時,就可以重用getJSON
的函式,直接把url當作参數傳進函式裏就行。
你懂 JavaScript 嗎?#24 Promise
Promise 对象
JavaScript Promise 全介紹
有筆誤, 在 「Promise物件的狀態 」 的小節裡:
「resolved:事件已完成,結果成功 略... 」, 應該不是 resolved 而是fulfilled