async 函數(shù)
含義
ES2017 標(biāo)準(zhǔn)引入了 async 函數(shù)峡蟋,使得異步操作變得更加方便似忧。
async
函數(shù)對 Generator 函數(shù)的改進项钮,體現(xiàn)在以下四點班眯。
(1)內(nèi)置執(zhí)行器希停。
Generator 函數(shù)的執(zhí)行必須靠執(zhí)行器,所以才有了co
模塊署隘,而async
函數(shù)自帶執(zhí)行器宠能。也就是說,async
函數(shù)的執(zhí)行磁餐,與普通函數(shù)一模一樣违崇,只要一行。
(2)更好的語義
async
和await
诊霹,比起星號和yield
羞延,語義更清楚了。async
表示函數(shù)里有異步操作脾还,await
表示緊跟在后面的表達式需要等待結(jié)果伴箩。
(3)更廣的適用性。
co
模塊約定鄙漏,yield
命令后面只能是 Thunk 函數(shù)或 Promise 對象嗤谚,而async
函數(shù)的await
命令后面,可以是 Promise 對象和原始類型的值(數(shù)值怔蚌、字符串和布爾值巩步,但這時會自動轉(zhuǎn)成立即 resolved 的 Promise 對象)。
(4)返回值是 Promise桦踊。
async
函數(shù)的返回值是 Promise 對象椅野,這比 Generator 函數(shù)的返回值是 Iterator 對象方便多了。你可以用then
方法指定下一步的操作钞钙。
進一步說鳄橘,async
函數(shù)完全可以看作多個異步操作,包裝成的一個 Promise 對象芒炼,而await
命令就是內(nèi)部then
命令的語法糖瘫怜。
基本用法
async
函數(shù)返回一個 Promise 對象,可以使用then
方法添加回調(diào)函數(shù)本刽。當(dāng)函數(shù)執(zhí)行的時候鲸湃,一旦遇到await
就會先返回,等到異步操作完成子寓,再接著執(zhí)行函數(shù)體內(nèi)后面的語句暗挑。
返回 Promise 對象
async
函數(shù)返回一個 Promise 對象。
async
函數(shù)內(nèi)部return
語句返回的值斜友,會成為then
方法回調(diào)函數(shù)的參數(shù)
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
上面代碼中炸裆,函數(shù)f
內(nèi)部return
命令返回的值,會被then
方法回調(diào)函數(shù)接收到鲜屏。
async
函數(shù)內(nèi)部拋出錯誤烹看,會導(dǎo)致返回的 Promise 對象變?yōu)?code>reject狀態(tài)国拇。拋出的錯誤對象會被catch
方法回調(diào)函數(shù)接收到。
Promise 對象的狀態(tài)變化
async
函數(shù)返回的 Promise 對象惯殊,必須等到內(nèi)部所有await
命令后面的 Promise 對象執(zhí)行完酱吝,才會發(fā)生狀態(tài)改變,除非遇到return
語句或者拋出錯誤土思。也就是說务热,只有async
函數(shù)內(nèi)部的異步操作執(zhí)行完,才會執(zhí)行then
方法指定的回調(diào)函數(shù)己儒。
await 命令
正常情況下崎岂,await
命令后面是一個 Promise 對象,返回該對象的結(jié)果址愿。如果不是 Promise 對象该镣,就直接返回對應(yīng)的值。
另一種情況是响谓,await
命令后面是一個thenable
對象(即定義then
方法的對象),那么await
會將其等同于 Promise 對象省艳。
有時娘纷,我們希望即使前一個異步操作失敗,也不要中斷后面的異步操作跋炕。這時可以將第一個await
放在try...catch
結(jié)構(gòu)里面赖晶,這樣不管這個異步操作是否成功,第二個await
都會執(zhí)行辐烂。
錯誤處理
如果await
后面的異步操作出錯遏插,那么等同于async
函數(shù)返回的 Promise 對象被reject
。 防止出錯的方法纠修,也是將其放在try...catch
代碼塊之中胳嘲。
async 函數(shù)的實現(xiàn)原理
async 函數(shù)的實現(xiàn)原理,就是將 Generator 函數(shù)和自動執(zhí)行器扣草,包裝在一個函數(shù)里了牛。
Promise 的含義
Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強大辰妙。它由社區(qū)最早提出和實現(xiàn)鹰祸,ES6 將其寫進了語言標(biāo)準(zhǔn),統(tǒng)一了用法密浑,原生提供了Promise
對象蛙婴。
所謂Promise
,簡單說就是一個容器尔破,里面保存著某個未來才會結(jié)束的事件(通常是一個異步操作)的結(jié)果街图。從語法上說浇衬,Promise 是一個對象,從它可以獲取異步操作的消息台夺。Promise 提供統(tǒng)一的 API径玖,各種異步操作都可以用同樣的方法進行處理。
Promise
對象有以下兩個特點颤介。
(1)對象的狀態(tài)不受外界影響梳星。Promise
對象代表一個異步操作,有三種狀態(tài):pending
(進行中)滚朵、fulfilled
(已成功)和rejected
(已失斣稳怠)。只有異步操作的結(jié)果蚕断,可以決定當(dāng)前是哪一種狀態(tài)迹冤,任何其他操作都無法改變這個狀態(tài)。這也是Promise
這個名字的由來移宅,它的英語意思就是“承諾”归粉,表示其他手段無法改變。
(2)一旦狀態(tài)改變漏峰,就不會再變糠悼,任何時候都可以得到這個結(jié)果。Promise
對象的狀態(tài)改變浅乔,只有兩種可能:從pending
變?yōu)?code>fulfilled和從pending
變?yōu)?code>rejected倔喂。只要這兩種情況發(fā)生,狀態(tài)就凝固了靖苇,不會再變了席噩,會一直保持這個結(jié)果,這時就稱為 resolved(已定型)贤壁。如果改變已經(jīng)發(fā)生了悼枢,你再對Promise
對象添加回調(diào)函數(shù),也會立即得到這個結(jié)果芯砸。這與事件(Event)完全不同萧芙,事件的特點是,如果你錯過了它假丧,再去監(jiān)聽双揪,是得不到結(jié)果的。
注意包帚,為了行文方便渔期,本章后面的resolved
統(tǒng)一只指fulfilled
狀態(tài),不包含rejected
狀態(tài)。
有了Promise
對象疯趟,就可以將異步操作以同步操作的流程表達出來拘哨,避免了層層嵌套的回調(diào)函數(shù)。此外信峻,Promise
對象提供統(tǒng)一的接口倦青,使得控制異步操作更加容易。
Promise
也有一些缺點盹舞。首先产镐,無法取消Promise
,一旦新建它就會立即執(zhí)行踢步,無法中途取消癣亚。其次,如果不設(shè)置回調(diào)函數(shù)获印,Promise
內(nèi)部拋出的錯誤述雾,不會反應(yīng)到外部。第三兼丰,當(dāng)處于pending
狀態(tài)時玻孟,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
基本用法
ES6 規(guī)定鳍征,Promise
對象是一個構(gòu)造函數(shù)取募,用來生成Promise
實例。
下面代碼創(chuàng)造了一個Promise
實例蟆技。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise
構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別是resolve
和reject
斗忌。它們是兩個函數(shù)质礼,由 JavaScript 引擎提供,不用自己部署织阳。
resolve
函數(shù)的作用是眶蕉,將Promise
對象的狀態(tài)從“未完成”變?yōu)椤俺晒Α保磸?pending 變?yōu)?resolved),在異步操作成功時調(diào)用唧躲,并將異步操作的結(jié)果造挽,作為參數(shù)傳遞出去;reject
函數(shù)的作用是弄痹,將Promise
對象的狀態(tài)從“未完成”變?yōu)椤笆 保磸?pending 變?yōu)?rejected)饭入,在異步操作失敗時調(diào)用,并將異步操作報出的錯誤肛真,作為參數(shù)傳遞出去谐丢。
Promise
實例生成以后,可以用then
方法分別指定resolved
狀態(tài)和rejected
狀態(tài)的回調(diào)函數(shù)。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then
方法可以接受兩個回調(diào)函數(shù)作為參數(shù)乾忱。第一個回調(diào)函數(shù)是Promise
對象的狀態(tài)變?yōu)?code>resolved時調(diào)用讥珍,第二個回調(diào)函數(shù)是Promise
對象的狀態(tài)變?yōu)?code>rejected時調(diào)用。其中窄瘟,第二個函數(shù)是可選的衷佃,不一定要提供。這兩個函數(shù)都接受Promise
對象傳出的值作為參數(shù)蹄葱。
下面是一個Promise
對象的簡單例子氏义。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
});
上面代碼中,timeout
方法返回一個Promise
實例新蟆,表示一段時間以后才會發(fā)生的結(jié)果觅赊。過了指定的時間(ms
參數(shù))以后,Promise
實例的狀態(tài)變?yōu)?code>resolved琼稻,就會觸發(fā)then
方法綁定的回調(diào)函數(shù)吮螺。
Promise 新建后就會立即執(zhí)行。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
上面代碼中帕翻,Promise 新建后立即執(zhí)行鸠补,所以首先輸出的是Promise
。然后嘀掸,then
方法指定的回調(diào)函數(shù)紫岩,將在當(dāng)前腳本所有同步任務(wù)執(zhí)行完才會執(zhí)行,所以resolved
最后輸出睬塌。
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
上面代碼中泉蝌,調(diào)用resolve(1)
以后,后面的console.log(2)
還是會執(zhí)行揩晴,并且會首先打印出來勋陪。這是因為立即 resolved 的 Promise 是在本輪事件循環(huán)的末尾執(zhí)行,總是晚于本輪循環(huán)的同步任務(wù)硫兰。
一般來說诅愚,調(diào)用resolve
或reject
以后,Promise 的使命就完成了劫映,后繼操作應(yīng)該放到then
方法里面违孝,而不應(yīng)該直接寫在resolve
或reject
的后面。所以泳赋,最好在它們前面加上return
語句雌桑,這樣就不會有意外。
new Promise((resolve, reject) => {
return resolve(1);
// 后面的語句不會執(zhí)行
console.log(2);
})
Promise.prototype.then()
Promise 實例具有then
方法摹蘑,也就是說筹燕,then
方法是定義在原型對象Promise.prototype
上的。它的作用是為 Promise 實例添加狀態(tài)改變時的回調(diào)函數(shù)。前面說過撒踪,then
方法的第一個參數(shù)是resolved
狀態(tài)的回調(diào)函數(shù)过咬,第二個參數(shù)(可選)是rejected
狀態(tài)的回調(diào)函數(shù)。
then
方法返回的是一個新的Promise
實例(注意制妄,不是原來那個Promise
實例)掸绞。因此可以采用鏈?zhǔn)綄懛ǎ?code>then方法后面再調(diào)用另一個then
方法耕捞。
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
上面的代碼使用then
方法衔掸,依次指定了兩個回調(diào)函數(shù)。第一個回調(diào)函數(shù)完成以后俺抽,會將返回結(jié)果作為參數(shù)敞映,傳入第二個回調(diào)函數(shù)。
Promise.prototype.catch()
Promise.prototype.catch
方法是.then(null, rejection)
或.then(undefined, rejection)
的別名磷斧,用于指定發(fā)生錯誤時的回調(diào)函數(shù)振愿。
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 處理 getJSON 和 前一個回調(diào)函數(shù)運行時發(fā)生的錯誤
console.log('發(fā)生錯誤!', error);
});
上面代碼中弛饭,getJSON
方法返回一個 Promise 對象冕末,如果該對象狀態(tài)變?yōu)?code>resolved,則會調(diào)用then
方法指定的回調(diào)函數(shù)侣颂;如果異步操作拋出錯誤档桃,狀態(tài)就會變?yōu)?code>rejected,就會調(diào)用catch
方法指定的回調(diào)函數(shù)憔晒,處理這個錯誤藻肄。另外,then
方法指定的回調(diào)函數(shù)拒担,如果運行中拋出錯誤仅炊,也會被catch
方法捕獲。
Promise.prototype.finally()
finally
方法用于指定不管 Promise 對象最后狀態(tài)如何澎蛛,都會執(zhí)行的操作。該方法是 ES2018 引入標(biāo)準(zhǔn)的蜕窿。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代碼中谋逻,不管promise
最后的狀態(tài),在執(zhí)行完then
或catch
指定的回調(diào)函數(shù)以后桐经,都會執(zhí)行finally
方法指定的回調(diào)函數(shù)毁兆。
Promise.all()
Promise.all()
方法用于將多個 Promise 實例,包裝成一個新的 Promise 實例阴挣。
const p = Promise.all([p1, p2, p3]);
上面代碼中气堕,Promise.all()
方法接受一個數(shù)組作為參數(shù),p1
、p2
茎芭、p3
都是 Promise 實例揖膜,如果不是,就會先調(diào)用下面講到的Promise.resolve
方法梅桩,將參數(shù)轉(zhuǎn)為 Promise 實例壹粟,再進一步處理。另外宿百,Promise.all()
方法的參數(shù)可以不是數(shù)組趁仙,但必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例垦页。
p
的狀態(tài)由p1
雀费、p2
、p3
決定痊焊,分成兩種情況盏袄。
(1)只有p1
、p2
宋光、p3
的狀態(tài)都變成fulfilled
貌矿,p
的狀態(tài)才會變成fulfilled
,此時p1
罪佳、p2
逛漫、p3
的返回值組成一個數(shù)組,傳遞給p
的回調(diào)函數(shù)赘艳。
(2)只要p1
酌毡、p2
、p3
之中有一個被rejected
蕾管,p
的狀態(tài)就變成rejected
枷踏,此時第一個被reject
的實例的返回值,會傳遞給p
的回調(diào)函數(shù)掰曾。
Promise.race()
Promise.race()
方法同樣是將多個 Promise 實例旭蠕,包裝成一個新的 Promise 實例。
const p = Promise.race([p1, p2, p3]);
上面代碼中旷坦,只要p1
掏熬、p2
、p3
之中有一個實例率先改變狀態(tài)秒梅,p
的狀態(tài)就跟著改變旗芬。那個率先改變的 Promise 實例的返回值,就傳遞給p
的回調(diào)函數(shù)捆蜀。
Promise.race()
方法的參數(shù)與Promise.all()
方法一樣疮丛,如果不是 Promise 實例幔嫂,就會先調(diào)用下面講到的Promise.resolve()
方法,將參數(shù)轉(zhuǎn)為 Promise 實例誊薄,再進一步處理履恩。
下面是一個例子,如果指定時間內(nèi)沒有獲得結(jié)果暇屋,就將 Promise 的狀態(tài)變
Promise.allSettled()
Promise.allSettled()
方法接受一組 Promise 實例作為參數(shù)似袁,包裝成一個新的 Promise 實例。只有等到所有這些參數(shù)實例都返回結(jié)果咐刨,不管是fulfilled
還是rejected
昙衅,包裝實例才會結(jié)束。該方法由 ES2020 引入定鸟。
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator();
上面代碼對服務(wù)器發(fā)出三個請求而涉,等到三個請求都結(jié)束,不管請求成功還是失敗联予,加載的滾動圖標(biāo)就會消失啼县。
該方法返回的新的 Promise 實例,一旦結(jié)束沸久,狀態(tài)總是fulfilled
季眷,不會變成rejected
。狀態(tài)變成fulfilled
后卷胯,Promise 的監(jiān)聽函數(shù)接收到的參數(shù)是一個數(shù)組子刮,每個成員對應(yīng)一個傳入Promise.allSettled()
的 Promise 實例。
Promise.any()
Promise.any()
方法接受一組 Promise 實例作為參數(shù)窑睁,包裝成一個新的 Promise 實例挺峡。只要參數(shù)實例有一個變成fulfilled
狀態(tài),包裝實例就會變成fulfilled
狀態(tài)担钮;如果所有參數(shù)實例都變成rejected
狀態(tài)橱赠,包裝實例就會變成rejected
狀態(tài)。該方法目前是一個第三階段的提案 箫津。
Promise.any()
跟Promise.race()
方法很像狭姨,只有一點不同,就是不會因為某個 Promise 變成rejected
狀態(tài)而結(jié)束苏遥。
const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c'),
];
try {
const first = await Promise.any(promises);
console.log(first);
} catch (error) {
console.log(error);
}
上面代碼中送挑,Promise.any()
方法的參數(shù)數(shù)組包含三個 Promise 操作。其中只要有一個變成fulfilled
暖眼,Promise.any()
返回的 Promise 對象就變成fulfilled
。如果所有三個操作都變成rejected
纺裁,那么就會await
命令就會拋出錯誤诫肠。
Promise.any()
拋出的錯誤司澎,不是一個一般的錯誤,而是一個 AggregateError 實例栋豫。它相當(dāng)于一個數(shù)組挤安,每個成員對應(yīng)一個被rejected
的操作所拋出的錯誤。
Promise.resolve()
有時需要將現(xiàn)有對象轉(zhuǎn)為 Promise 對象丧鸯,Promise.resolve()
方法就起到這個作用蛤铜。
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
上面代碼將 jQuery 生成的deferred
對象,轉(zhuǎn)為一個新的 Promise 對象丛肢。
Promise.resolve()
等價于下面的寫法围肥。
Promise.resolve('foo')
// 等價于
new Promise(resolve => resolve('foo'))
Promise.resolve
方法的參數(shù)分成四種情況。
(1)參數(shù)是一個 Promise 實例
如果參數(shù)是 Promise 實例蜂怎,那么Promise.resolve
將不做任何修改穆刻、原封不動地返回這個實例。
(2)參數(shù)是一個thenable
對象
thenable
對象指的是具有then
方法的對象杠步,比如下面這個對象氢伟。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
Promise.resolve
方法會將這個對象轉(zhuǎn)為 Promise 對象,然后就立即執(zhí)行thenable
對象的then
方法幽歼。
(3)參數(shù)不是具有then
方法的對象朵锣,或根本就不是對象
如果參數(shù)是一個原始值,或者是一個不具有then
方法的對象甸私,則Promise.resolve
方法返回一個新的 Promise 對象诚些,狀態(tài)為resolved
。
const p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
上面代碼生成一個新的 Promise 對象的實例p
颠蕴。由于字符串Hello
不屬于異步操作(判斷方法是字符串對象不具有 then 方法)泣刹,返回 Promise 實例的狀態(tài)從一生成就是resolved
,所以回調(diào)函數(shù)會立即執(zhí)行犀被。Promise.resolve
方法的參數(shù)椅您,會同時傳給回調(diào)函數(shù)。
(4)不帶有任何參數(shù)
Promise.resolve()
方法允許調(diào)用時不帶參數(shù)寡键,直接返回一個resolved
狀態(tài)的 Promise 對象掀泳。
所以,如果希望得到一個 Promise 對象西轩,比較方便的方法就是直接調(diào)用Promise.resolve()
方法员舵。
Promise.reject()
Promise.reject(reason)
方法也會返回一個新的 Promise 實例,該實例的狀態(tài)為rejected
藕畔。
const p = Promise.reject('出錯了');
// 等同于
const p = new Promise((resolve, reject) => reject('出錯了'))
p.then(null, function (s) {
console.log(s)
});
// 出錯了
上面代碼生成一個 Promise 對象的實例p
马僻,狀態(tài)為rejected
,回調(diào)函數(shù)會立即執(zhí)行注服。
注意韭邓,Promise.reject()
方法的參數(shù)措近,會原封不動地作為reject
的理由,變成后續(xù)方法的參數(shù)女淑。這一點與Promise.resolve
方法不一致瞭郑。