首先明確一個(gè)問題蚊伞,為什么 Node.js 需要異步編程?
JavaScript 是單線程的兜蠕,在發(fā)出一個(gè)調(diào)用時(shí)扰肌,在沒有得到結(jié)果之前,該調(diào)用就不返回熊杨,意思就是調(diào)用者主動等待調(diào)用結(jié)果曙旭,換句話說,就是必須等待上一個(gè)任務(wù)執(zhí)行完才能執(zhí)行下一個(gè)任務(wù)這種執(zhí)行模式叫:同步晶府。
Node.js 的主要應(yīng)用場景是處理高并發(fā)(單位時(shí)間內(nèi)極大的訪問量)和 I/O 密集場景(ps: I/O 操作往往非常耗時(shí)桂躏,所以異步的關(guān)鍵在于解決I/O耗時(shí)問題),如果采用同步編程川陆,問題就來了剂习,服務(wù)器處理一個(gè) I/O 請求需要大量的時(shí)間,后面的請求都將排隊(duì)较沪,造成瀏覽器端的卡頓鳞绕。異步編程能解決這個(gè)問題。
所謂異步尸曼,就是調(diào)用在發(fā)出后们何,這個(gè)調(diào)用就直接返回了,調(diào)用者不會立即得到結(jié)果并且可以繼續(xù)執(zhí)行后續(xù)操作控轿,而被調(diào)用者執(zhí)行得到結(jié)果后通過狀態(tài)冤竹、事件來通知調(diào)用者使用回調(diào)函數(shù)(callback)來處理這個(gè)調(diào)用。Node在處理耗時(shí)的 I/O 操作時(shí)茬射,將其交給其他線程處理鹦蠕,自己繼續(xù)處理其他訪問請求,當(dāng) I/O 操作處理好后就會通過事件通知 Node 用回調(diào)做后續(xù)處理在抛。
有個(gè)例子非常好:
你打電話問書店老板有沒有《分布式系統(tǒng)》這本書钟病,如果是同步通信機(jī)制,書店老板會說刚梭,你稍等肠阱,”我查一下",然后開始查啊查望浩,等查好了(可能是5秒辖所,也可能是一天)告訴你結(jié)果(返回結(jié)果)。而異步通信機(jī)制磨德,書店老板直接告訴你我查一下啊缘回,查好了打電話給你吆视,然后直接掛電話了(不返回結(jié)果)。然后查好了酥宴,他會主動打電話給你啦吧。在這里老板通過“回電”這種方式來回調(diào)。
下面幾種方式是異步解決方案的進(jìn)化過程:
CallBacks
回調(diào)函數(shù)就是函數(shù)A作為參數(shù)傳遞給函數(shù)B拙寡,并且在未來某一個(gè)時(shí)間被調(diào)用授滓。callback的異步模式最大的問題就是,理解困難加回調(diào)地獄(callback hell)肆糕,看下面的代碼的執(zhí)行順序:
A();
ajax('url1', function(){
B();
ajax('url2', function(){
C();
}
D();
});
E();
其執(zhí)行順序?yàn)椋篈 => E => B => D => C般堆,這種執(zhí)行順序的確會讓人頭腦發(fā)昏,另外由于由于多個(gè)異步操作之間往往會耦合诚啃,只要中間一個(gè)操作需要修改淮摔,那么它的上層回調(diào)函數(shù)和下層回調(diào)函數(shù)都可能要修改,這就陷入了回調(diào)地獄始赎。而 Promise 對象就很好的解決了異步操作之間的耦合問題和橙,讓我們可以用同步編程的方式去寫異步操作。
Promise
Promise 對象是一個(gè)構(gòu)造函數(shù)造垛,用來生成promise實(shí)例魔招。Promise 代表一個(gè)異步操作,有三種狀態(tài):pending五辽,resolved(異步操作成功由pending變?yōu)閞esolved)办斑,rejected(異步操作失敗由pending變?yōu)閞ejected),一旦變?yōu)楹髢煞N狀態(tài)將不會再改變奔脐。Promise 對象作為構(gòu)造函數(shù)接受一個(gè)函數(shù)作為參數(shù)俄周,而這個(gè)函數(shù)又接受 resolved 和 rejected 兩個(gè)函數(shù)做為參數(shù)吁讨,這兩個(gè)函數(shù)是JS內(nèi)置的髓迎,無需配置。resolved 函數(shù)在異步操作成功后調(diào)用建丧,將pending狀態(tài)變?yōu)閞esolved排龄,并將它的參數(shù)傳遞給回調(diào)函數(shù);rejected 函數(shù)在異步操作失敗時(shí)調(diào)用翎朱,將pending狀態(tài)變?yōu)閞ejected橄维,并將參數(shù)傳遞給回調(diào)函數(shù)。
Promise.prototype.then()
Promise構(gòu)造函數(shù)的原型上有一個(gè)then方法拴曲,它接受兩個(gè)函數(shù)作為參數(shù)争舞,分別是 resolved 狀態(tài)和 rejected 狀態(tài)的回調(diào)函數(shù),而這兩個(gè)回調(diào)函數(shù)接受的參數(shù)分別是Promise實(shí)例中resolved函數(shù)和rejected返回的結(jié)果, 另外第二個(gè)函數(shù)是可選的澈灼。
下面是一個(gè)示例:
const instance = new Promise((resolved, rejected) => {
// 一些異步操作
if(/*異步操作成功*/) {
resolved(value);
} else {
rejected(error);
}
}
})
instance.then(value => {
// do something...
}, error => {
// do something...
})
注意Promise實(shí)例在生成后會立即執(zhí)行竞川,而then方法只有在所有同步任務(wù)執(zhí)行完后才會執(zhí)行(疑問)店溢,看看下面的例子:
const promise = new Promise((resolved, rejected) => {
console.log('async task begins!');
setTimeout(() => {
resolved('done, pending -> resolved!');
}, 1000);
})
promise.then(value => {
console.log(value);
})
console.log('1.please wait');
console.log('2.please wait');
console.log('3.please wait');
// async task begins!
// 1.please wait
// 2.please wait
// 3.please wait
// done, pending -> resolved!
上面的實(shí)例可以看出,Promise新建后立即執(zhí)行委乌,所以首先輸出 'async task begins!'床牧,隨后定義了一個(gè)異步操作 setTimeout,1秒后執(zhí)行遭贸,所以無需等待戈咳,向下執(zhí)行,而then方法指定的回調(diào)函數(shù)要在所有同步任務(wù)執(zhí)行完后才執(zhí)行壕吹,所以先輸出了3個(gè)'please wait'著蛙,最后輸出'done, pending -> resolved!'。(此處省略了then方法中的rejected回調(diào)耳贬,一般不在then中做rejected狀態(tài)的處理册踩,而使用catch方法專門處理錯(cuò)誤)
鏈?zhǔn)秸{(diào)用then方法
then方法會將返回值傳遞給下一個(gè)回調(diào)(即return后面的值),如果返回的是Promise實(shí)例效拭,則可以繼續(xù)調(diào)用then方法暂吉,而如果返回的是值,則相當(dāng)于執(zhí)行了 promise.resolve()方法缎患,也可以繼續(xù)調(diào)用then方法慕的,這就是then的鏈?zhǔn)綄懛ǎ错樞驅(qū)崿F(xiàn)一系列的異步操作挤渔,這樣就可以用同步編程的形式去實(shí)現(xiàn)異步操作肮街,來看下面的例子:
function timeAdder(n) {
return new Promise((resolved, rejected) => {
setTimeout(() => {
resolved(n + 200);
}, n)
})
}
timeAdder(0)
.then(n => {
console.log(`time1 is ${n}`);
return timeAdder(n); // 最終 resolved 函數(shù)中的參數(shù)將作為值傳遞給下一個(gè)then
})
// n 是上一個(gè)then傳遞出來的參數(shù)
.then(n => {
console.log(`time2 is ${n}`);
return timeAdder(n);
})
.then(n => {
console.log(`time3 is ${n}`);
return timeAdder(n);
})
// time1 is 200
// time3 is 400
// time3 is 600
可以看到使用鏈?zhǔn)絫hen的寫法,將異步操作變成了同步的形式判导,但是也帶來了新的問題嫉父,就是異步操作變成了很長的then鏈,新的解決方法就是Generator眼刃,這里跨過它直接說它的語法糖:async/await绕辖。
async/await
async
async/await實(shí)際上是Generator的語法糖。顧名思義擂红,async關(guān)鍵字代表后面的函數(shù)中有異步操作仪际,await表示等待一個(gè)異步方法執(zhí)行完成。聲明一步函數(shù)只需在普通函數(shù)前面加一個(gè)關(guān)鍵字async即可昵骤,如:
async function funcA() {}
async 函數(shù)返回一個(gè)Promise對象(如果結(jié)果是值树碱,會經(jīng)過Promise包裝返回),因此asyn函數(shù)通過return返回的值变秦,會成為then方法的參數(shù):
async function funcA() {
return 'hello!';
}
funcA().then(value => {
console.log(value);
})
// hello!
單獨(dú)一個(gè)async函數(shù)成榜,其實(shí)與Promise實(shí)例執(zhí)行的功能是一樣的,來看看await都干了些啥蹦玫。
await
await 命令后面的值可以是Promise 對象或值赎婚,如果是值雨饺,就會被轉(zhuǎn)成一個(gè)立即resolve的Promise對象。async函數(shù)被調(diào)用后就立即執(zhí)行惑淳,但是一旦遇到await就會先返回额港,等到異步操作執(zhí)行完成,再接著執(zhí)行函數(shù)體內(nèi)后面的語句歧焦,看看下面這個(gè)例子:
async function func() {
console.log('async function is running!');
const num1 = await 200;
console.log(`num1 is ${num1}`);
const num2 = await num1+ 100;
console.log(`num2 is ${num2}`);
const num3 = await num2 + 100;
console.log(`num3 is ${num3}`);
}
func();
console.log('run me before await!');
// async function is running!
// run me before await!
// num1 is 200
// num2 is 300
// num3 is 400
可以看出調(diào)用 async func 函數(shù)后移斩,它會立即執(zhí)行,首先輸出了'async function is running!'绢馍,接著遇到了await異步等待向瓷,函數(shù)返回,先執(zhí)行func()后面的同步任務(wù)舰涌,同步任務(wù)執(zhí)行完后猖任,接著await等待的位置繼續(xù)往下執(zhí)行。
值得注意的是瓷耙,await 后面的 Promise 對象不總是返回 resolved 狀態(tài)朱躺,只要一個(gè)await后面的Promise狀態(tài)變?yōu)閞ejected,整個(gè)async函數(shù)都會中斷執(zhí)行搁痛,為了保存錯(cuò)誤的位置和錯(cuò)誤信息长搀,我們需要用 try...catch 語句來封裝多個(gè)await過程,如下:
async function func() {
try {
const num1 = await 200;
console.log(`num1 is ${num1}`);
const num2 = await Promise.reject('num2 is wrong!');
console.log(`num2 is ${num2}`);
const num3 = await num2 + 100;
console.log(`num3 is ${num3}`);
} catch (error) {
console.log(error);
}
}
func();
// num1 is 200
// 出錯(cuò)了
// num2 is wrong!
如上所示鸡典,在num2處await得到了一個(gè)狀態(tài)為rejected的Promise對象源请,該錯(cuò)誤會被傳遞到catch語句中,這樣我們就可以定位錯(cuò)誤發(fā)生的位置彻况。
async/await比Promise強(qiáng)在哪兒谁尸?
接下來我們用async/await改寫一下Promise章節(jié)中關(guān)于timeAdder的一個(gè)例子,代碼如下:
function timeAdder(n) {
return new Promise((resolved, rejected) => {
setTimeout(() => {
resolved(n + 200);
}, n)
})
}
async function func() {
const time1 = await timeAdder(0);
console.log(`time1 is ${time1}`);
const time2 = await timeAdder(time1);
console.log(`time3 is ${time2}`);
const time3 = await timeAdder(time2);
console.log(`time3 is ${time3}`);
}
func();
// time1 is 200
// time3 is 400
// time3 is 600
與之前長長的then鏈和then方法里的回調(diào)函數(shù)相比纽甘,這樣的寫法是不是更加清爽良蛮,更加符合編程習(xí)慣?
參考文章
https://segmentfault.com/a/1190000007535316
https://segmentfault.com/a/1190000006138882
https://www.zhihu.com/question/19732473/answer/20851256