回調(diào)地獄
首先有一個(gè)需求,如何連續(xù)根據(jù)函數(shù)的依賴關(guān)系,實(shí)現(xiàn)多個(gè)函數(shù)的連續(xù)調(diào)用,而且要在前置函數(shù)完成的情況下。例如 1 秒鐘之后執(zhí)行 fn1
;fn1
執(zhí)行完畢,相隔 1 秒,執(zhí)行 fn2
;fn2
執(zhí)行完畢,相隔 1 秒,執(zhí)行 fn3
。
我們可以利用回調(diào)函數(shù),將后續(xù)需要執(zhí)行的函數(shù)作為前置函數(shù)的回調(diào)函數(shù)參數(shù),在前置函數(shù)執(zhí)行之后執(zhí)行。
// exp 1
function fn1(callback) {
setTimeout(()=>{
console.log('fn1 executed');
callback();
},1000);
}
function fn2(callback) {
setTimeout(()=>{
console.log('fn2 executed');
callback();
},1000);
}
function fn3() {
setTimeout(()=>{
console.log('fn3 executed');
},1000);
}
fn1(function() {
fn2(function() {
fn3();
});
});
// "fn1 executed"
// 1s~
// "fn2 executed"
// 1s~
// "fn3 executed"
上述代碼中不斷嵌入回調(diào)函數(shù),回調(diào)函數(shù)中還有函數(shù)作為參數(shù),結(jié)果輸出沒有問題。但是代碼缺乏可讀性和拓展性,健壯性。當(dāng)其中一個(gè)函數(shù)需要修改,或者嵌套回調(diào)層數(shù)增多,將陷入常說的“回調(diào)地獄”中,我們需要一種更為符合邏輯,更優(yōu)雅的異步回調(diào)的方法—— Promise。
Promise 的含義
在 MDN 文檔 中,它是被這樣定義的:
Promise 對象用于表示一個(gè)異步操作的最終狀態(tài)(完成或失敗),以及該異步操作的結(jié)果值。
Promise 對象是一個(gè)代理對象(代理一個(gè)值),被代理的值在Promise對象創(chuàng)建時(shí)可能是未知的。它允許你為異步操作的成功和失敗分別綁定相應(yīng)的處理方法(handlers)。 這讓異步方法可以像同步方法那樣返回值,但并不是立即返回最終執(zhí)行結(jié)果,而是一個(gè)能代表未來出現(xiàn)的結(jié)果的 Promise 對象
阮一峰老師的 理解:
Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強(qiáng)大。它由社區(qū)最早提出和實(shí)現(xiàn),ES6 將其寫進(jìn)了語言標(biāo)準(zhǔn),統(tǒng)一了用法,原生提供了Promise對象。
Promise 通過對構(gòu)造函數(shù)的設(shè)計(jì),讓異步編程比傳統(tǒng)的方式更合理,處理更為邏輯化,代碼也更加優(yōu)雅。
語法
Promise 對象具有兩個(gè)特點(diǎn):1)Promise 對象分別有三種狀態(tài) pending
,fullfiled
,rejected
。最開始時(shí)是pending
,當(dāng)內(nèi)部異步函數(shù)執(zhí)行成功,則狀態(tài)立即變?yōu)?code>fullfilled,且不可更改;當(dāng)內(nèi)部異步函數(shù)執(zhí)行失敗,則狀態(tài)立即變?yōu)閞ejected,且不可更改。2)Promise 對象定義后會(huì)立即執(zhí)行,而 resolve
函數(shù)要等待響應(yīng)的結(jié)果。
// exp 2
const promiseExp = new Promise(function(resolve, rejected) {
if(/*success condition */) {
resolve(value);
}else {
reject(error);
}
});
function ifSuccess() {
// do somthing if success
}
function ifFailure(error) {
// do somthing if fail
}
promiseExp().then(ifSuccess).catch(ifFailure(error));
上述代碼中,當(dāng) resolve
或者 reject
被執(zhí)行,Promise 狀態(tài)從pending
(進(jìn)行中) 變成fullfilled
(完成)或者 rejected
(失敗);當(dāng)其中任意一個(gè)發(fā)生時(shí),就會(huì)調(diào)用相對應(yīng)的函數(shù),成功后該執(zhí)行的函數(shù)或失敗后該執(zhí)行的函數(shù),且由于.then()
和 .catch
方法依舊返回一個(gè) Promise 對象,::因此可以鏈?zhǔn)秸{(diào)用,類似于解決文章開頭的“回調(diào)地獄”的問題。
Promise 改造:
// exp 3
function fn1() {
return new Promise(function(resolve, reject) {
console.log('fn1 promise immediatly');
setTimeout(()=>{
console.log('fn1 async');
resolve();
},1000);
});
}
function fn2() {
return new Promise(function(resolve, reject) {
console.log('fn2 promise immediatly');
setTimeout(()=>{
console.log('fn2 async');
resolve();
},1000);
});
}
function fn3() {
return new Promise(function(resolve, reject) {
console.log('fn3 promise immediatly');
setTimeout(()=>{
console.log('fn3 async');
resolve();
},1000);
});
}
function onerror() {
console.log('error');
}
fn1().then(fn2).then(fn3).catch(onerror);
console.log('outer after calling');
/*
"fn1 promise immediatly"
"outer after calling"
1s~
"fn1 async"
"fn2 promise immediatly"
1s~
"fn2 async"
"fn3 promise immediatly"
1s~
"fn3 async"
*/
上述代碼對最初的代碼進(jìn)行了改造,我們可以看到幾點(diǎn):
1)通過狀態(tài)的變化,觸發(fā).then()
函數(shù)中的函數(shù),我們避免了多層的回調(diào)函數(shù)嵌套,以同步的方式進(jìn)行異步函數(shù)回調(diào),更具有可讀性,合理性和健壯性。
2)Promise 對象是立即執(zhí)行的,體現(xiàn)在1s的間距,"fn1 immediatly" 是和 “outer after calling ”一起輸出的,而其他的都是上一個(gè)異步函數(shù)(fnx async )和 異步完成調(diào)用函數(shù)(fnx+1 promise immediatly)一起輸出的。
3).then()
返回的仍是一個(gè) Promise 對象,因此在連續(xù)的回調(diào)函數(shù)依賴關(guān)系中,通過對 promise.prototype.then
的連續(xù)鏈?zhǔn)秸{(diào)用,實(shí)現(xiàn)了連續(xù)的函數(shù)回調(diào)(如下圖)。
Promise 的原型
Promise.prototype.then()
添加解決(fulfillment
)和拒絕(rejection
)回調(diào)到當(dāng)前 Promise, 返回一個(gè)新的 Promise, 將以回調(diào)的返回值來resolve
。
then(onfulfilled, onrejected)
,then()
有兩個(gè)參數(shù),一個(gè)是 resolve
狀態(tài)的回調(diào)函數(shù),一個(gè)是 reject
狀態(tài)的回調(diào)函數(shù)(可選),分別對應(yīng)onfullfilled
,onrejected
;根據(jù)上面的例子,then
返回的仍是 Promise 對象,因此可以實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用。
// exp 4
getIp("/getIp.php").then(
ipResult => getCity(ipResult.ip)
).then(
city => getWeather(city),
err => console.log('rejected: ' + err);
)
上面的代碼中,第一個(gè)then()
指定的回調(diào)函數(shù)getCity
返回的仍是一個(gè) Promise 對象,因此繼續(xù)調(diào)用then()
,此時(shí),如果第二個(gè)then
指定的回調(diào)函數(shù)就會(huì)等待新的返回的 Promise 對象狀態(tài)的變化,如果是狀態(tài)變?yōu)?resolved,則執(zhí)行getWeather
,如果是rejected,則執(zhí)行console.log('rejected: '+err)
;
Promise.prototype.catch
和then()
一樣,catch()
方法返回的是一個(gè) Promise 對象,以及他拒絕的理由,他的行為和Promise.prototype.then(undefined, onRejected)
。實(shí)際上,ECMA 的 官方文檔 就是這么寫的:obj.catch(onRejected)等同于obj.then(undefined, onRejected)
在
catch
的使用中,他不僅可以捕獲來自源 Promise 對象拋出的錯(cuò)誤(下面第一個(gè)例子),也同時(shí)可以捕獲在鏈?zhǔn)秸{(diào)用then
和catch
時(shí),由then
拋出的錯(cuò)誤(第二個(gè)例子)。
// exp 5
const promise = new Promise(function(resolve, reject) {
console.log('before throw');
throw new Error('error test');
console.log('after throw');
});
promise.catch(function(error) {
console.log(error);
});
// "before throw"
// "error test"
上面的代碼中,我們定義了一個(gè) Promise 對象,他的作用就是拋出一個(gè)錯(cuò)誤,并將錯(cuò)誤的內(nèi)容傳遞出去,當(dāng) Promise 對象調(diào)用catch
捕獲的時(shí)候,它可以直接捕獲由 Promise 傳遞出的 error,并且立即執(zhí)行完畢throw
,錯(cuò)誤被捕捉之后,就不再執(zhí)行之后的函數(shù),因此在throw
之后的log
語句沒有被執(zhí)行出來。
// exp 6
const promise = new Promise(function(resolve,reject) {
resolve('error test');
});
promise.then(function(err){
console.log('now I throw an error');
throw new Error(err);
}).catch(function(err){
console.log(err);
});
// "now I throw an error"
// "error test"
上面的代碼和前一段不同,在 Promise 對象中,并沒有拋出錯(cuò)誤。錯(cuò)誤時(shí)在then
的回調(diào)函數(shù)中拋出的,可以看到,catch
不僅可以捕獲來自第一個(gè) Promise 的錯(cuò)誤,由于鏈?zhǔn)秸{(diào)用的原因,還可以捕獲then()
回調(diào)函數(shù)返回的 Promise 對象的錯(cuò)誤。
前面說道:
Promise 對象具有兩個(gè)特點(diǎn):1)Promise 對象分別有三種狀態(tài)
pending
,fullfiled
,rejected
。最開始時(shí)是pending
,當(dāng)內(nèi)部異步函數(shù)執(zhí)行成功,則狀態(tài)立即變?yōu)?code>fullfilled,且不可更改;當(dāng)內(nèi)部異步函數(shù)執(zhí)行失敗,則狀態(tài)立即變?yōu)?code>rejected,且不可更改。
如果是已經(jīng)執(zhí)行resolve
之后,狀態(tài)變成了fullfilled
,再拋出錯(cuò)誤,會(huì)不會(huì)被catch
捕獲呢?
// exp 7
const promise = new Promise(function(resolve,reject) {
resolve('The ink is dry');
throw new Error('An error after resolve');
});
promise.then(function(msg){
console.log(msg);
}).catch(function(err){
console.log(err);
});
// "The ink is dry"
不會(huì),因?yàn)?Promise 的狀態(tài)已經(jīng)從pending
變成fullfilled
,就不會(huì)改變,同理如 exp 5 的 Promise 對象中的 console.log
語句,在throw
語句之前的log
被執(zhí)行了,之后的log
沒有被執(zhí)行,因?yàn)?code>throw之后,Promise 的狀態(tài)已經(jīng)改變了,就不會(huì)再繼續(xù)執(zhí)行下面的代碼。
Promise.prototype.finally
finally()
方法返回一個(gè) Promise。在 Promise 結(jié)束時(shí),無論結(jié)果是fulfilled
或者是rejected
,都會(huì)執(zhí)行指定的回調(diào)函數(shù)。這為在Promise 是否成功完成后都需要執(zhí)行的代碼提供了一種方式。這避免了同樣的語句需要在then()
和catch()
中各寫一次的情況。
// exp 8
promise()
.then(val =>{/* do something success */})
.catch(err =>{/* do something fail */})
.finally(() => {/* do something whatever*/})
上面使用的案例中,我們規(guī)定了成功該做什么是,并傳入一個(gè)val
值,規(guī)定了失敗該做什么時(shí),并傳入錯(cuò)誤原因,最終,我們無論成功失敗,都要完成的事情,它并沒有輸入的參數(shù),也無法從它確定Promise 的狀態(tài)。
阮一峰老師試著實(shí)現(xiàn)了了finally函數(shù)
// exp 9
Promise.prototype.finally = funtion (callback) {
let P = this.constructor;
return this.then(
val => P.resolve(callback()).then( () => val),
err => P.resolve(callback()).then( () => err)
);
}
Promise 的方法
Promise.all
.all
方法的參數(shù)是一個(gè) Promise 對象列表,而返回的仍是一個(gè)Promise 對象,當(dāng)輸入的所有的 Promise 對象狀態(tài)都為resolved
時(shí),返回的 Promise 新對象才返回resolve
,當(dāng)有一個(gè)出現(xiàn)reject
時(shí),則新返回的 Promise 返回reject
,錯(cuò)誤原因是第一個(gè)出現(xiàn)失敗的 Promise 的結(jié)果。
// exp 10
var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then(function(values) {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
上面代碼中,promise.all
等待輸入的三個(gè) Promise 均完成后,盡管一些 Promise 沒有包含異步函數(shù),但是結(jié)果其結(jié)果仍然被放進(jìn)了最終返回的 Promise 中。
在作為參數(shù)列表的 Promise 對象中,如果他有自己的catch
函數(shù),當(dāng)他拋出錯(cuò)誤時(shí),他的錯(cuò)誤將被自己的catch
捕獲,而不會(huì)被promise.all
的catch
捕獲。
// exp 11
let p1 = new Promise(function(resolve, reject) {
resolve('p1 is ok');
}).then(result => result);
let p2 = new Promise(function(resolve, reject) {
throw new Error('error test');
}).then(function(msg) {
console.log(msg);
}).catch(function(err){
console.log('p2 err captrue: '+ err);
});
let promise = Promise.all([p1,p2]);
promise.then(function(msg){
console.log('promiseAll msg: '+msg);
}).catch(function(err){
console.log('promiseAll err captrue: '+err);
});
/*
"p2 err captrue: Error: error test"
["p1 is ok", undefined]
*/
上面的代碼中,p1 狀態(tài)為resolved
,并將resolve
的值"p1 is ok"作為結(jié)果傳入返回的回調(diào)函數(shù)中;p2 則拋出了一個(gè)錯(cuò)誤,但是這個(gè)錯(cuò)誤被 p2 本身的catch
函數(shù)捕捉到了,catch
函數(shù)捕捉到之后,返回一個(gè)新的 Promise,此時(shí)這個(gè) Promise 的狀態(tài)是resolved
,因此,當(dāng)使用 Promise.all([p1, p2])
的時(shí)候,兩者的狀態(tài)都為resolved
,只是 p2 沒有返回的值,因此輸出中,p2 的值是"undefined",如果 p2 沒有自己的catch
方法,則在Promsie.all([p1,p2])
中則會(huì)調(diào)用catch
方法。
Promsie.race
Promise.race
和Promise.all
方法輸入的參數(shù)一致,都是一個(gè)參數(shù)數(shù)組,只是.race
是一旦參數(shù)數(shù)組中的某一個(gè) Promsie 完成(resolve)
或者拒絕(reject)
,狀態(tài)更改,他就會(huì)返回一個(gè)新的Promsie,狀態(tài)和參數(shù)列表中的第一個(gè)發(fā)生狀態(tài)改變的 Promsie 一致。
// exp 12
var p1 = new Promise(function(resolve, reject) {
setTimeout(resolve,100,'promise one 100ms');
});
var p2 = new Promise(function(resolve, reject) {
setTimeout(resolve,200,'promise two 200ms');
});
var promise = Promise.race([p1,p2]).then(function(result){
console.log(result);
});
// ""promise one 100ms""
上面的代碼中,設(shè)置了兩個(gè) Promise 對象參數(shù),但是設(shè)置了不同的異步完成的時(shí)間,p1 比 p2 快 100ms,因此在 p1 狀態(tài)發(fā)生改變,從pending
到resolved
之后,Promise.race
立即返回新的Promise
對象,狀態(tài)和 p1 一直,傳遞的值就是 p1 的值。
Promise.resolve
Promise.resolve
方法返回一個(gè)給定解析值的 Promise 對象,也就是將現(xiàn)有對象轉(zhuǎn)化為 Promise 對象。傳入的參數(shù)可以是一個(gè) Promise 對象,也可以是一個(gè) thenable
。
靜態(tài)使用 resolve
方法
// exp 13
Promise.resolve('resolve exp').then(function(msg){
console.log(msg);
},function(err){
console.log('Error:'+err); // 不會(huì)執(zhí)行
});
// "resolve exp"
上面代碼中,resolve
方法直接返回一個(gè)新的 Promise 對象,并且處于 fullfilled
狀態(tài),攜帶的 value
是 "resolve exp" 因此,新的 Promise 對象直接調(diào)用.then
方法。
參數(shù)是一個(gè)thenable
對象
// exp 14
let thenable = {
then:function(resolve, reject) {
resolve('resolved before throw');
reject('after resolve');
}
};
var p = Promise.resolve(thenable);
p.then(function(msg) {
console.log(msg);
},function(err){
console.log('error: ' + err);
});
上面的代碼中,resolve
輸入的是一個(gè) thenable
對象,resolve
方法會(huì)將這個(gè)對象轉(zhuǎn)為 Promise 對象,然后執(zhí)行thenable
對象的then
方法,執(zhí)行后p
的狀態(tài)將變?yōu)?code>resolved,因此p
的then
方法將會(huì)被執(zhí)行,輸出thenable
傳遞的msg
。
需要注意的是,立即resolve()
的 Promise 對象,是在本輪“事件循環(huán)”(event loop)的結(jié)束時(shí)執(zhí)行,而不是在下一輪“事件循環(huán)”的開始時(shí)。
// exp 15
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
/*
"one"
"two"
"three"
*/
上面的代碼中,setTimeout()
是在下一輪時(shí)間循環(huán)開始時(shí)執(zhí)行,Promise
在本輪時(shí)間循環(huán)結(jié)束時(shí)執(zhí)行,console.log
立即執(zhí)行,因此最先輸出。
Promise.reject
本方法返回一個(gè)帶有拒絕原因的Promise
對象,該對象的狀態(tài)自然為rejected
。
靜態(tài)使用reject
方法
// exp 16
Promise.reject('reject exp').then(function(reason) {
console.log(reason)},
function(reason) {
console.log('Error: ' + reason) ;
});
//Error: reject exp
上面代碼生成一個(gè) Promise 對象的實(shí)例p,狀態(tài)為rejected
,回調(diào)函數(shù)會(huì)立即執(zhí)行。
與resolve
方法不同的是,reject
方法傳入的參數(shù),會(huì)作為后續(xù)方法的理由,而不是像resolve
一樣,將原 thenable 傳入的參數(shù)傳遞。
// exp 17
let thenable = {
then:function(resolve, reject) {
reject('reject exp');
}
};
var p = Promise.reject(thenable);
p.then(null,function(e){
console.log('target: ' + e);
});
// "target: [object Object] "
上面函數(shù)中,傳遞到p.then
中的參數(shù)不是"reject exp"
字符串,而是thenable
對象本身。
await async
await
操作符用于等待一個(gè) Promise 對象。它只能在異步函數(shù) async function
中使用。使用 Promise 配合 await 和 async,我們已經(jīng)可以像書寫同步函數(shù)那樣書寫異步函數(shù)。
// exp 18
function resolve2second(x) {
return new Promise(resolve => {
setTimeout(()=>{
resolve(x);
},2000);
});
};
async function fn1() {
console.log('fn1 immediatly');
var x = await resolve2second(10);
console.log(x)
}
fn1();
/*
"fn1 immediatly"
// 2s~
10
*/
上面代碼中,resolve2second
是一個(gè)2秒后執(zhí)行的異步函數(shù),在async 函數(shù)fn1
中,設(shè)置了await
表達(dá)式,使得x變量賦值的操作暫停,等待Promise結(jié)果出來后,由返回的resolve值再執(zhí)行對x的賦權(quán),而fn1
函數(shù)的內(nèi)的console.log
函數(shù)不受影響,隨fn1
立即執(zhí)行
參考閱讀
- Promise 對象,阮一峰。
- Promise;await;async_function,MDN。