回調(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é)果輸出沒(méi)有問(wèn)題。但是代碼缺乏可讀性和拓展性谓谦,健壯性贫橙。當(dāng)其中一個(gè)函數(shù)需要修改,或者嵌套回調(diào)層數(shù)增多反粥,將陷入常說(shuō)的“回調(diào)地獄”中卢肃,我們需要一種更為符合邏輯,更優(yōu)雅的異步回調(diào)的方法—— Promise才顿。
Promise 的含義
在 MDN 文檔 中莫湘,它是被這樣定義的:
Promise 對(duì)象用于表示一個(gè)異步操作的最終狀態(tài)(完成或失敗)郑气,以及該異步操作的結(jié)果值幅垮。
Promise 對(duì)象是一個(gè)代理對(duì)象(代理一個(gè)值),被代理的值在Promise對(duì)象創(chuàng)建時(shí)可能是未知的尾组。它允許你為異步操作的成功和失敗分別綁定相應(yīng)的處理方法(handlers)忙芒。 這讓異步方法可以像同步方法那樣返回值,但并不是立即返回最終執(zhí)行結(jié)果讳侨,而是一個(gè)能代表未來(lái)出現(xiàn)的結(jié)果的 Promise 對(duì)象
阮一峰老師的 理解:
Promise 是異步編程的一種解決方案匕争,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強(qiáng)大。它由社區(qū)最早提出和實(shí)現(xiàn)爷耀,ES6 將其寫進(jìn)了語(yǔ)言標(biāo)準(zhǔn)甘桑,統(tǒng)一了用法,原生提供了Promise對(duì)象歹叮。
Promise 通過(guò)對(duì)構(gòu)造函數(shù)的設(shè)計(jì)跑杭,讓異步編程比傳統(tǒng)的方式更合理,處理更為邏輯化咆耿,代碼也更加優(yōu)雅德谅。
語(yǔ)法
Promise 對(duì)象具有兩個(gè)特點(diǎn):1)Promise 對(duì)象分別有三種狀態(tài) pending
,fullfiled
萨螺,rejected
窄做。最開(kāi)始時(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 對(duì)象定義后會(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)用相對(duì)應(yīng)的函數(shù)乐横,成功后該執(zhí)行的函數(shù)或失敗后該執(zhí)行的函數(shù)句旱,且由于.then()
和 .catch
方法依舊返回一個(gè) Promise 對(duì)象,::因此可以鏈?zhǔn)秸{(diào)用晰奖,類似于解決文章開(kāi)頭的“回調(diào)地獄”的問(wèn)題谈撒。
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"
*/
上述代碼對(duì)最初的代碼進(jìn)行了改造,我們可以看到幾點(diǎn):
1)通過(guò)狀態(tài)的變化匾南,觸發(fā).then()
函數(shù)中的函數(shù)啃匿,我們避免了多層的回調(diào)函數(shù)嵌套,以同步的方式進(jìn)行異步函數(shù)回調(diào)蛆楞,更具有可讀性溯乒,合理性和健壯性。
2)Promise 對(duì)象是立即執(zhí)行的豹爹,體現(xiàn)在1s的間距裆悄,"fn1 immediatly" 是和 “outer after calling ”一起輸出的,而其他的都是上一個(gè)異步函數(shù)(fnx async )和 異步完成調(diào)用函數(shù)(fnx+1 promise immediatly)一起輸出的臂聋。
3).then()
返回的仍是一個(gè) Promise 對(duì)象光稼,因此在連續(xù)的回調(diào)函數(shù)依賴關(guān)系中,通過(guò)對(duì) 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)的返回值來(lái)resolve
。
then(onfulfilled, onrejected)
肄方,then()
有兩個(gè)參數(shù)冰垄,一個(gè)是 resolve
狀態(tài)的回調(diào)函數(shù),一個(gè)是 reject
狀態(tài)的回調(diào)函數(shù)(可選)权她,分別對(duì)應(yīng)onfullfilled
虹茶,onrejected
逝薪;根據(jù)上面的例子,then
返回的仍是 Promise 對(duì)象蝴罪,因此可以實(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 對(duì)象洲炊,因此繼續(xù)調(diào)用then()
感局,此時(shí)尼啡,如果第二個(gè)then
指定的回調(diào)函數(shù)就會(huì)等待新的返回的 Promise 對(duì)象狀態(tài)的變化暂衡,如果是狀態(tài)變?yōu)?resolved,則執(zhí)行getWeather
崖瞭,如果是rejected狂巢,則執(zhí)行console.log('rejected: '+err)
;
Promise.prototype.catch
和then()
一樣,catch()
方法返回的是一個(gè) Promise 對(duì)象书聚,以及他拒絕的理由唧领,他的行為和Promise.prototype.then(undefined, onRejected)
。實(shí)際上雌续,ECMA 的 官方文檔 就是這么寫的:obj.catch(onRejected)等同于obj.then(undefined, onRejected)
在
catch
的使用中斩个,他不僅可以捕獲來(lái)自源 Promise 對(duì)象拋出的錯(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 對(duì)象鸽心,他的作用就是拋出一個(gè)錯(cuò)誤滚局,并將錯(cuò)誤的內(nèi)容傳遞出去,當(dāng) Promise 對(duì)象調(diào)用catch
捕獲的時(shí)候顽频,它可以直接捕獲由 Promise 傳遞出的 error藤肢,并且立即執(zhí)行完畢throw
,錯(cuò)誤被捕捉之后糯景,就不再執(zhí)行之后的函數(shù)嘁圈,因此在throw
之后的log
語(yǔ)句沒(méi)有被執(zhí)行出來(lái)。
// 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 對(duì)象中丑孩,并沒(méi)有拋出錯(cuò)誤。錯(cuò)誤時(shí)在then
的回調(diào)函數(shù)中拋出的灭贷,可以看到温学,catch
不僅可以捕獲來(lái)自第一個(gè) Promise 的錯(cuò)誤,由于鏈?zhǔn)秸{(diào)用的原因甚疟,還可以捕獲then()
回調(diào)函數(shù)返回的 Promise 對(duì)象的錯(cuò)誤仗岖。
前面說(shuō)道:
Promise 對(duì)象具有兩個(gè)特點(diǎn):1)Promise 對(duì)象分別有三種狀態(tài)
pending
逃延,fullfiled
,rejected
轧拄。最開(kāi)始時(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 對(duì)象中的 console.log
語(yǔ)句旅急,在throw
語(yǔ)句之前的log
被執(zhí)行了逢勾,之后的log
沒(méi)有被執(zhí)行,因?yàn)?code>throw之后藐吮,Promise 的狀態(tài)已經(jīng)改變了溺拱,就不會(huì)再繼續(xù)執(zhí)行下面的代碼。
Promise.prototype.finally
finally()
方法返回一個(gè) Promise炎码。在 Promise 結(jié)束時(shí)盟迟,無(wú)論結(jié)果是fulfilled
或者是rejected
,都會(huì)執(zhí)行指定的回調(diào)函數(shù)潦闲。這為在Promise 是否成功完成后都需要執(zhí)行的代碼提供了一種方式攒菠。這避免了同樣的語(yǔ)句需要在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ò)誤原因凹炸,最終,我們無(wú)論成功失敗昼弟,都要完成的事情啤它,它并沒(méi)有輸入的參數(shù),也無(wú)法從它確定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 對(duì)象列表变骡,而返回的仍是一個(gè)Promise 對(duì)象离赫,當(dāng)輸入的所有的 Promise 對(duì)象狀態(tài)都為resolved
時(shí),返回的 Promise 新對(duì)象才返回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 沒(méi)有包含異步函數(shù)接剩,但是結(jié)果其結(jié)果仍然被放進(jìn)了最終返回的 Promise 中切厘。
在作為參數(shù)列表的 Promise 對(duì)象中,如果他有自己的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 沒(méi)有返回的值冤馏,因此輸出中,p2 的值是"undefined"寄啼,如果 p2 沒(méi)有自己的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 對(duì)象參數(shù)驾茴,但是設(shè)置了不同的異步完成的時(shí)間戴陡,p1 比 p2 快 100ms,因此在 p1 狀態(tài)發(fā)生改變沟涨,從pending
到resolved
之后恤批,Promise.race
立即返回新的Promise
對(duì)象,狀態(tài)和 p1 一直裹赴,傳遞的值就是 p1 的值喜庞。
Promise.resolve
Promise.resolve
方法返回一個(gè)給定解析值的 Promise 對(duì)象,也就是將現(xiàn)有對(duì)象轉(zhuǎn)化為 Promise 對(duì)象棋返。傳入的參數(shù)可以是一個(gè) Promise 對(duì)象延都,也可以是一個(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 對(duì)象晰房,并且處于 fullfilled
狀態(tài),攜帶的 value
是 "resolve exp" 因此射沟,新的 Promise 對(duì)象直接調(diào)用.then
方法殊者。
參數(shù)是一個(gè)thenable
對(duì)象
// 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
對(duì)象验夯,resolve
方法會(huì)將這個(gè)對(duì)象轉(zhuǎn)為 Promise 對(duì)象猖吴,然后執(zhí)行thenable
對(duì)象的then
方法,執(zhí)行后p
的狀態(tài)將變?yōu)?code>resolved挥转,因此p
的then
方法將會(huì)被執(zhí)行海蔽,輸出thenable
傳遞的msg
。
需要注意的是绑谣,立即resolve()
的 Promise 對(duì)象党窜,是在本輪“事件循環(huán)”(event loop)的結(jié)束時(shí)執(zhí)行,而不是在下一輪“事件循環(huán)”的開(kāi)始時(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)開(kāi)始時(shí)執(zhí)行,Promise
在本輪時(shí)間循環(huán)結(jié)束時(shí)執(zhí)行暇务,console.log
立即執(zhí)行泼掠,因此最先輸出。
Promise.reject
本方法返回一個(gè)帶有拒絕原因的Promise
對(duì)象垦细,該對(duì)象的狀態(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 對(duì)象的實(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
對(duì)象本身。
await async
await
操作符用于等待一個(gè) Promise 對(duì)象右冻。它只能在異步函數(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é)果出來(lái)后肃叶,由返回的resolve值再執(zhí)行對(duì)x的賦權(quán)蹂随,而fn1
函數(shù)的內(nèi)的console.log
函數(shù)不受影響,隨fn1
立即執(zhí)行
參考閱讀
- Promise 對(duì)象被环,阮一峰糙及。
- Promise详幽;await筛欢;async_function,MDN唇聘。