一、async/await的優(yōu)點
1)方便級聯(lián)調(diào)用:即調(diào)用依次發(fā)生的場景;
2)同步代碼編寫方式: Promise使用then函數(shù)進行鏈式調(diào)用,一直點點點,是一種從左向右的橫向?qū)懛?;async/await從上到下,順序執(zhí)行,就像寫同步代碼一樣,更符合代碼編寫習慣;
3)多個參數(shù)傳遞: Promise的then函數(shù)只能傳遞一個參數(shù),雖然可以通過包裝成對象來傳遞多個參數(shù),但是會導致傳遞冗余信息,頻繁的解析又重新組合參數(shù),比較麻煩;async/await沒有這個限制,可以當做普通的局部變量來處理,用let或者const定義的塊級變量想怎么用就怎么用,想定義幾個就定義幾個,完全沒有限制,也沒有冗余工作;
4)同步代碼和異步代碼可以一起編寫: 使用Promise的時候最好將同步代碼和異步代碼放在不同的then節(jié)點中,這樣結(jié)構(gòu)更加清晰;async/await整個書寫習慣都是同步的,不需要糾結(jié)同步和異步的區(qū)別,當然,異步過程需要包裝成一個Promise對象放在await關(guān)鍵字后面;
5)基于協(xié)程: Promise是根據(jù)函數(shù)式編程的范式,對異步過程進行了一層封裝,async/await基于協(xié)程的機制,是真正的“保存上下文,控制權(quán)切換……控制權(quán)恢復,取回上下文”這種機制,是對異步過程更精確的一種描述;
6)async/await是對Promise的優(yōu)化: async/await是基于Promise的,是進一步的一種優(yōu)化,不過在寫代碼時,Promise本身的API出現(xiàn)得很少,很接近同步代碼的寫法;
二、協(xié)程
- 進程>線程>協(xié)程
- 協(xié)程的第一大優(yōu)勢是具有極高的執(zhí)行效率,因為子程序切換不是線程切換,而是由程序自身控制,因此沒有線程切換的開銷,和多線程比,線程數(shù)量越多,協(xié)程的性能優(yōu)勢就越明顯;
- 協(xié)程的第二大優(yōu)勢是不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協(xié)程中控制共享資源不加鎖,只需要判斷狀態(tài)就好了,所以執(zhí)行效率比多線程高很多;
- 協(xié)程看上去也是子程序,但執(zhí)行過程中,在子程序內(nèi)部可中斷,然后轉(zhuǎn)而執(zhí)行別的子程序,在適當?shù)臅r候再返回來接著執(zhí)行,需要注意的是:在一個子程序中中斷,去執(zhí)行其他子程序,這并不是函數(shù)調(diào)用,有點類似于CPU的中斷;
- 用汽車和公路舉個例子:js公路只是單行道(主線程),但是有很多車道(輔助線程)都可以匯入車流(異步任務完成后回調(diào)進入主線程的任務隊列);generator把js公路變成了多車道(協(xié)程實現(xiàn)),但是同一時間只有一個車道上的車能開(依然單線程),不過可以自由變道(移交控制權(quán));
- 協(xié)程意思是多個線程互相協(xié)作,完成異步任務,運行流程大致如下:
1)協(xié)程A開始執(zhí)行;
2)協(xié)程A執(zhí)行到一半,進入暫停,執(zhí)行權(quán)轉(zhuǎn)移到協(xié)程B;
3)一段時間后,協(xié)程B交還執(zhí)行權(quán);
4)協(xié)程A恢復執(zhí)行; - 協(xié)程是一個無優(yōu)先級的子程序調(diào)度組件,允許子程序在特定的地點掛起恢復;
- 線程包含于進程,協(xié)程包含于線程,只要內(nèi)存足夠,一個線程中可以有任意多個協(xié)程,但某一個時刻只能有一個協(xié)程在運行,多個協(xié)程分享該線程分配到的計算機資源;
- 就實際使用理解來說,協(xié)程允許我們寫同步代碼的邏輯,卻做著異步的事,避免了回調(diào)嵌套,使得代碼邏輯清晰;
- 何時掛起,喚醒協(xié)程:協(xié)程是為了使用異步的優(yōu)勢,異步操作是為了避免IO操作阻塞線程,那么協(xié)程掛起的時刻應該是當前協(xié)程發(fā)起異步操作的時候,而喚醒應該在其他協(xié)程退出,并且他的異步操作完成時;
- 單線程內(nèi)開啟協(xié)程,一旦遇到io,從應用程序級別(而非操作系統(tǒng))控制切換對比操作系統(tǒng)控制線程的切換,用戶在單線程內(nèi)控制協(xié)程的切換,優(yōu)點如下:
1)協(xié)程的切換開銷更小,屬于程序級別的切換,操作系統(tǒng)完全感知不到,因而更加輕量級;
2)單線程內(nèi)就可以實現(xiàn)并發(fā)的效果,最大限度地利用cpu;
// 傳統(tǒng)的生產(chǎn)者-消費者模型是一個線程寫消息,一個線程取消息,通過鎖機制控制隊列和等待,但一不小心就可能死鎖。
// 如果改用協(xié)程,生產(chǎn)者生產(chǎn)消息后,直接通過yield跳轉(zhuǎn)到消費者開始執(zhí)行,待消費者執(zhí)行完畢后,切換回生產(chǎn)者繼續(xù)生產(chǎn),效率極高:
import time
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
time.sleep(1)
r = '200 OK'
def produce(c):
c.next()
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
if __name__=='__main__':
c = consumer()
produce(c)
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
注意到consumer函數(shù)是一個generator(生成器),把一個consumer傳入produce后:
首先調(diào)用c.next()啟動生成器;
然后,一旦生產(chǎn)了東西,通過c.send(n)切換到consumer執(zhí)行;
consumer通過yield拿到消息,處理,又通過yield把結(jié)果傳回;
produce拿到consumer處理的結(jié)果,繼續(xù)生產(chǎn)下一條消息;
produce決定不生產(chǎn)了,通過c.close()關(guān)閉consumer,整個過程結(jié)束。
整個流程無鎖,由一個線程執(zhí)行,produce和consumer協(xié)作完成任務,所以稱為“協(xié)程”,而非線程的搶占式多任務。
三、async關(guān)鍵字
1)表明程序里面可能有異步過程: async關(guān)鍵字表明程序里面可能有異步過程,里面可以有await關(guān)鍵字;當然全部是同步代碼也沒關(guān)系,但是這樣async關(guān)鍵字就顯得多余了;
2)非阻塞: async函數(shù)里面如果有異步過程會等待,但是async函數(shù)本身會馬上返回,不會阻塞當前線程,可以簡單認為,async函數(shù)工作在主線程,同步執(zhí)行,不會阻塞界面渲染,async函數(shù)內(nèi)部由await關(guān)鍵字修飾的異步過程,工作在相應的協(xié)程上,會阻塞等待異步任務的完成再返回;
3)async函數(shù)返回類型為Promise對象: 這是和普通函數(shù)本質(zhì)上不同的地方,也是使用時重點注意的地方;
(1)return newPromise();這個符合async函數(shù)本意;
(2)return data;這個是同步函數(shù)的寫法,這里是要特別注意的,這個時候,其實就相當于Promise.resolve(data);還是一個Promise對象,但是在調(diào)用async函數(shù)的地方通過簡單的=是拿不到這個data的,因為返回值是一個Promise對象,所以需要用.then(data => { })函數(shù)才可以拿到這個data;
(3)如果沒有返回值,相當于返回了Promise.resolve(undefined);
4)無等待 聯(lián)想到Promise的特點,在沒有await的情況下執(zhí)行async函數(shù),它會立即執(zhí)行,返回一個Promise對象,并且絕對不會阻塞后面的語句,這和普通返回Promise對象的函數(shù)并無二致;
5)await不處理異步error: await是不管異步過程的reject(error)消息的,async函數(shù)返回的這個Promise對象的catch函數(shù)負責統(tǒng)一抓取內(nèi)部所有異步過程的錯誤;async函數(shù)內(nèi)部只要有一個異步過程發(fā)生錯誤,整個執(zhí)行過程就中斷,這個返回的Promise對象的catch就能抓取到這個錯誤;
5)async函數(shù)的執(zhí)行: async函數(shù)執(zhí)行和普通函數(shù)一樣,函數(shù)名帶個()就可以了,參數(shù)個數(shù)隨意,沒有限制,也需要有async關(guān)鍵字;只是返回值是一個Promise對象,可以用then函數(shù)得到返回值,用catch抓整個流程中發(fā)生的錯誤;
async function testAsync() {
return "hello async";
}
const result = testAsync(); // 返回一個Promise對象
console.log(result);
// async函數(shù)返回的是一個Promise對象,async函數(shù)(包括函數(shù)語句、函數(shù)表達式、Lambda表達式)會返回一個Promise對象,如果在函數(shù)中return一個直接量,async會把這個直接量通過Promise.resolve() 封裝成 Promise 對象;
// async函數(shù)返回的是一個Promise對象,所以在最外層不能用await獲取其返回值的情況,應該使用原始的方式:then()鏈來處理這個Promise對象
testAsync().then(v => {
console.log(v); // 輸出 hello async
});
四、await關(guān)鍵字
1)await只能在async函數(shù)內(nèi)部使用:不能放在普通函數(shù)里面,否則會報錯;
2)await關(guān)鍵字后面跟Promise對象:在Pending狀態(tài)時,相應的協(xié)程會交出控制權(quán),進入等待狀態(tài),這是協(xié)程的本質(zhì);
3)await是async wait的意思: wait的是resolve(data)的消息,并把數(shù)據(jù)data返回,比如下面代碼中,當Promise對象由Pending變?yōu)镽esolved的時候,變量a就等于data,然后再順序執(zhí)行下面的語句console.log(a),這真的是等待,真的是順序執(zhí)行,表現(xiàn)和同步代碼幾乎一模一樣;
const a = await new Promise((resolve, reject) => {
// async process ...
return resolve(data);
});
console.log(a);
4)await后面也可以跟同步代碼: 不過系統(tǒng)會自動將其轉(zhuǎn)化成一個Promsie對象,比如:
const a = await 'hello world'
// 相當于
const a = await Promise.resolve('hello world');
// 跟同步代碼是一樣的,還不如省事點,直接去掉await關(guān)鍵字
const a = 'hello world';
5)await對于失敗消息的處理: await只關(guān)心異步過程成功的消息resolve(data),拿到相應的數(shù)據(jù)data,至于失敗消息reject(error),不關(guān)心不處理;對于錯誤的處理有以下幾種方法供選擇:
(1)讓await后面的Promise對象自己catch;
(2)也可以讓外面的async函數(shù)返回的Promise對象統(tǒng)一catch;
(3)像同步代碼一樣,放在一個try...catch結(jié)構(gòu)中;
async componentDidMount() { // 這是React Native的回調(diào)函數(shù),加個async關(guān)鍵字,沒有任何影響,但是可以用await關(guān)鍵字
// 將異步和同步的代碼放在一個try..catch中,異常都能抓到
try {
let array = null;
let data = await asyncFunction(); // 這里用await關(guān)鍵字,就能拿到結(jié)果值;否則,沒有await的話,只能拿到Promise對象
if (array.length > 0) { // 這里會拋出異常,下面的catch也能抓到
array.push(data);
}
} catch (error) {
alert(JSON.stringify(error))
}
}
6)await對于結(jié)果的處理: await是個運算符,用于組成表達式,await表達式的運算結(jié)果取決于它等的東西,如果它等到的不是一個Promise對象,那么await表達式的運算結(jié)果就是它等到的東西;如果它等到的是一個Promise對象,await就忙起來了,它會阻塞其后面的代碼,等著Promise對象resolve,然后得到resolve的值,作為await表達式的運算結(jié)果;雖然是阻塞,但async函數(shù)調(diào)用并不會造成阻塞,它內(nèi)部所有的阻塞都被封裝在一個Promise對象中異步執(zhí)行,這也正是await必須用在async函數(shù)中的原因;
五、套路分析一
// 異步過程封裝
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve('sleep for ' + ms + ' ms');
}, ms);
});
}
// 定義異步流程,可以將按照需要定制,就像寫同步代碼那樣
async function asyncFunction() {
console.time('asyncFunction total executing:');
const sleep1 = await sleep(2000);
console.log('sleep1: ' + sleep1);
const [sleep2, sleep3, sleep4]= await Promise.all([sleep(2000), sleep(1000), sleep(1500)]);
console.log('sleep2: ' + sleep2);
console.log('sleep3: ' + sleep3);
console.log('sleep4: ' + sleep4);
const sleepRace = await Promise.race([sleep(3000), sleep(1000), sleep(1000)]);
console.log('sleep race: ' + sleepRace);
console.timeEnd('asyncFunction total executing:');
return 'asyncFunction done.' // 這個可以不返回,這里只是做個標記,為了顯示流程
}
// 像普通函數(shù)調(diào)用async函數(shù),在then函數(shù)中獲取整個流程的返回信息,在catch函數(shù)統(tǒng)一處理出錯信息
asyncFunction().then(data => {
console.log(data); // asyncFunction return 的內(nèi)容在這里獲取
}).catch(error => {
console.log(error); // asyncFunction 的錯誤統(tǒng)一在這里抓取
});
console.log('after asyncFunction code executing....'); // 這個代表asyncFunction函數(shù)后的代碼,
// 顯示asyncFunction本身會立即返回,不會阻塞主線程
// 執(zhí)行結(jié)果
after asyncFunction code executing....
sleep1: sleep for 2000 ms
sleep2: sleep for 2000 ms
sleep3: sleep for 1000 ms
sleep4: sleep for 1500 ms
sleep race: sleep for 1000 ms
asyncFunction total executing:: 5006.276123046875ms
asyncFunction done.
代碼分析
after asyncFunction code executing....代碼位置在async函數(shù)asyncFunction()調(diào)用之后,反而先輸出,這說明async函數(shù)asyncFunction()調(diào)用之后會馬上返回,不會阻塞主線程;
sleep1: sleep for 2000 ms這是第一個await之后的第一個異步過程,最先執(zhí)行,也最先完成,說明后面的代碼,不論是同步和異步,都在等他執(zhí)行完畢;
sleep2 ~ sleep4這是第二個await之后的Promise.all()異步過程,這是“比慢模式”,三個sleep都完成后,再運行下面的代碼,耗時最長的是2000ms;
sleep race: sleep for 1000 ms這是第三個await之后的Promise.race()異步過程,這是“比快模式”,耗時最短sleep都完成后,就運行下面的代碼,耗時最短的是1000ms;
asyncFunction total executing:: 5006.276123046875ms這是最后的統(tǒng)計總共運行時間代碼,三個await之后的異步過程之和:
1000(獨立的) + 2000(Promise.all) + 1000(Promise.race) = 5000ms
這個和統(tǒng)計出來的5006.276123046875ms非常接近,說明上面的異步過程,和同步代碼執(zhí)行過程一致,協(xié)程真的是在等待異步過程執(zhí)行完畢;asyncFunction done.這個是async函數(shù)返回的信息,在執(zhí)行時的then函數(shù)中獲得,說明整個流程完畢之后參數(shù)傳遞的過程;
六、套路分析二
/**
* 傳入?yún)?shù) n,表示這個函數(shù)執(zhí)行的時間(毫秒)
* 執(zhí)行的結(jié)果是 n + 200,這個值將用于下一步驟
*/
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
// Promise方式調(diào)用
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms
// async/await方式調(diào)用
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
七、套路分析三
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(m, n) {
console.log(`step2 with ${m} and ${n}`);
return takeLongTime(m + n);
}
function step3(k, m, n) {
console.log(`step3 with ${k}, ${m} and ${n}`);
return takeLongTime(k + m + n);
}
// Promise方式調(diào)用
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => {
return step2(time1, time2)
.then(time3 => [time1, time2, time3]);
})
.then(times => {
const [time1, time2, time3] = times;
return step3(time1, time2, time3);
})
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
// async/await方式調(diào)用
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time1, time2);
const result = await step3(time1, time2, time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms
參考鏈接:
JavaScript:async/await的基礎(chǔ)用法
理解JavaScript的async/await