前言 正文從這開(kāi)始~ JS 中最基礎(chǔ)的異步調(diào)用方式是 callback,它將回調(diào)函數(shù) callback 傳給異步 API,由瀏覽器或 Node 在異步完成后,通知 JS 引擎調(diào)用 callback。對(duì)于簡(jiǎn)單的異步操作,用 callback 實(shí)現(xiàn),是夠用的。但隨著負(fù)責(zé)交互頁(yè)面和 Node 出現(xiàn),callback 方案的弊端開(kāi)始浮現(xiàn)出來(lái)。 Promise 規(guī)范孕育而生,并被納入 ES6 的規(guī)范中。后來(lái) ES7 又在 Promise 的基礎(chǔ)上將 async 函數(shù)納入標(biāo)準(zhǔn)。此為 JavaScript 異步進(jìn)化史。 同步與異步 通常,代碼是由上往下依次執(zhí)行的。如果有多個(gè)任務(wù),就必需排隊(duì),前一個(gè)任務(wù)完成,后一個(gè)任務(wù)才會(huì)執(zhí)行。這種執(zhí)行模式稱(chēng)之為:同步(synchronous)。新手容易把計(jì)算機(jī)用語(yǔ)中的同步,和日常用語(yǔ)中的同步弄混淆。如,“把文件同步到云端”中的同步,指的是“使...保持一致”。而在計(jì)算機(jī)中,同步指的是任務(wù)從上往下依次執(zhí)行的模式。比如: 在這段代碼中,A、B、C是三個(gè)不同的函數(shù),每個(gè)函數(shù)都是一個(gè)不相關(guān)的任務(wù)。在同步模式,計(jì)算機(jī)會(huì)先執(zhí)行 A 任務(wù),再執(zhí)行 B 任務(wù),最后執(zhí)行 C 任務(wù)。在大部分情況,同步模式都沒(méi)問(wèn)題。但是如果 B 任務(wù)是一個(gè)耗時(shí)很長(zhǎng)的網(wǎng)絡(luò)請(qǐng)求,而 C 任務(wù)恰好是展現(xiàn)新頁(yè)面,就會(huì)導(dǎo)致網(wǎng)頁(yè)卡頓。 更好解決方案是,將 B 任務(wù)分成兩個(gè)部分。一部分立即執(zhí)行網(wǎng)絡(luò)請(qǐng)求的任務(wù),另一部分在請(qǐng)求回來(lái)后的執(zhí)行任務(wù)。這種一部分立即執(zhí)行,另一部分在未來(lái)執(zhí)行的模式稱(chēng)為異步。 實(shí)際上,JS 引擎并沒(méi)有直接處理網(wǎng)絡(luò)請(qǐng)求的任務(wù),它只是調(diào)用了瀏覽器的網(wǎng)絡(luò)請(qǐng)求接口,由瀏覽器發(fā)送網(wǎng)絡(luò)請(qǐng)求并監(jiān)聽(tīng)返回的數(shù)據(jù)。JavaScript 異步能力的本質(zhì)是瀏覽器或 Node 的多線(xiàn)程能力。 callback 未來(lái)執(zhí)行的函數(shù)通常也叫 callback。使用 callback 的異步模式,解決了阻塞的問(wèn)題,但是也帶來(lái)了一些其他問(wèn)題。在最開(kāi)始,我們的函數(shù)是從上往下書(shū)寫(xiě)的,也是從上往下執(zhí)行的,這種“線(xiàn)性”模式,非常符合我們的思維習(xí)慣,但是現(xiàn)在卻被 callback 打斷了!在上面一段代碼中,現(xiàn)在它跳過(guò) B 任務(wù)先執(zhí)行了 C任務(wù)!這種異步“非線(xiàn)性”的代碼會(huì)比同步“線(xiàn)性”的代碼,更難閱讀,因此也更容易滋生 BUG。 試著判斷下面這段代碼的執(zhí)行順序,你會(huì)對(duì)“非線(xiàn)性”代碼比“線(xiàn)性”代碼更難以閱讀,體會(huì)更深。 這段代碼中,從上往下執(zhí)行的順序被 Callback 打亂了。我們的閱讀代碼視線(xiàn)是A => B => C => D => E,但是執(zhí)行順序卻是A => E => B => D => C,這就是非線(xiàn)性代碼帶來(lái)的糟糕之處。 通過(guò)將ajax后面執(zhí)行的任務(wù)提前,可以更容易看懂代碼的執(zhí)行順序。雖然代碼因?yàn)榍短卓雌饋?lái)不美觀(guān),但現(xiàn)在的執(zhí)行順序卻是從上到下的“線(xiàn)性”方式。這種技巧在寫(xiě)多重嵌套的代碼時(shí),是非常有用的。 上一段代碼只有處理了成功回調(diào),并沒(méi)處理異?;卣{(diào)。接下來(lái),把異常處理回調(diào)加上,再來(lái)討論代碼“線(xiàn)性”執(zhí)行的問(wèn)題。 加上異常處理回調(diào)后,url1的成功回調(diào)函數(shù) B 和異常回調(diào)函數(shù) E,被分開(kāi)了。這種“非線(xiàn)性”的情況又出現(xiàn)了。 在 node 中,為了解決的異?;卣{(diào)導(dǎo)致的“非線(xiàn)性”的問(wèn)題,制定了錯(cuò)誤優(yōu)先的策略。node 中 callback 的第一個(gè)參數(shù),專(zhuān)門(mén)用于判斷是否發(fā)生異常。 到此,callback 引起的“非線(xiàn)性”問(wèn)題基本得到解決。遺憾的是,使用 callback 嵌套,一層層if else和回調(diào)函數(shù),一旦嵌套層數(shù)多起來(lái),閱讀起來(lái)不是很方便。此外,callback 一旦出現(xiàn)異常,只能在當(dāng)前回調(diào)函數(shù)內(nèi)部處理異常。 promise 在 JavaScript 的異步進(jìn)化史中,涌現(xiàn)出一系列解決 callback 弊端的庫(kù),而 Promise 成為了最終的勝者,并成功地被引入了 ES6 中。它將提供了一個(gè)更好的“線(xiàn)性”書(shū)寫(xiě)方式,并解決了異步異常只能在當(dāng)前回調(diào)中被捕獲的問(wèn)題。 Promise 就像一個(gè)中介,它承諾會(huì)將一個(gè)可信任的異步結(jié)果返回。首先 Promise 和異步接口簽訂一個(gè)協(xié)議,成功時(shí),調(diào)用resolve函數(shù)通知 Promise,異常時(shí),調(diào)用reject通知 Promise。另一方面 Promise 和 callback 也簽訂一個(gè)協(xié)議,由 Promise 在將來(lái)返回可信任的值給then和catch中注冊(cè)的 callback。 Promise 是個(gè)非常不錯(cuò)的中介,它只返回可信的信息給 callback。它對(duì)第三方異步庫(kù)的結(jié)果進(jìn)行了一些加工,保證了 callback 一定會(huì)被異步調(diào)用,且只會(huì)被調(diào)用一次。 介紹完 Promise 的特性后,來(lái)看看它如何利用鏈?zhǔn)秸{(diào)用,解決異步代碼可讀性的問(wèn)題的。 如此反復(fù),不斷返回一個(gè) Promise 對(duì)象,再采用鏈?zhǔn)秸{(diào)用的方式不斷地調(diào)用。使 Promise 擺脫了 callback 層層嵌套的問(wèn)題和異步代碼“非線(xiàn)性”執(zhí)行的問(wèn)題。 Promise 解決的另外一個(gè)難點(diǎn)是 callback 只能捕獲當(dāng)前錯(cuò)誤異常。Promise 和 callback 不同,每個(gè) callback 只能知道自己的報(bào)錯(cuò)情況,但 Promise 代理著所有的 callback,所有 callback 的報(bào)錯(cuò),都可以由 Promise 統(tǒng)一處理。所以,可以通過(guò)catch來(lái)捕獲之前未捕獲的異常。 Promise 解決了 callback 的異步調(diào)用問(wèn)題,但 Promise 并沒(méi)有擺脫 callback,它只是將 callback 放到一個(gè)可以信任的中間機(jī)構(gòu),這個(gè)中間機(jī)構(gòu)去鏈接我們的代碼和異步接口。 異步(async)函數(shù) 異步(async)函數(shù)是 ES7 的一個(gè)新的特性,它結(jié)合了 Promise,讓我們擺脫 callback 的束縛,直接用類(lèi)同步的“線(xiàn)性”方式,寫(xiě)異步函數(shù)。 聲明異步函數(shù),只需在普通函數(shù)前添加一個(gè)關(guān)鍵字 async 即可,如async function main(){} 。在異步函數(shù)中,可以使用await關(guān)鍵字,表示等待后面表達(dá)式的執(zhí)行結(jié)果,一般后面的表達(dá)式是 Promise 實(shí)例。 異步函數(shù)和普通函數(shù)一樣調(diào)用 main() 。調(diào)用后,會(huì)立即執(zhí)行異步函數(shù)中的第一行代碼 var value = await timer(100) 。等到異步執(zhí)行完成后,才會(huì)執(zhí)行下一行代碼。 除此之外,異步函數(shù)和其他函數(shù)基本類(lèi)似,它使用try...catch來(lái)捕捉異常。也可以傳入?yún)?shù)。但不要在異步函數(shù)中使用return來(lái)返回值。 異步函數(shù)也可以被當(dāng)作值,傳入普通函數(shù)和異步函數(shù)中執(zhí)行。但是在異步函數(shù)中,使用異步函數(shù)時(shí)要注意,如果不使用await,異步函數(shù)會(huì)被同步執(zhí)行。 這個(gè)時(shí)候打印出來(lái)的值是 B A。說(shuō)明 doAsync 函數(shù)并沒(méi)有等待 main 的異步執(zhí)行完畢就執(zhí)行了 console。如果要讓 console 在 main 的異步執(zhí)行完畢后才執(zhí)行,我們需要在main前添加關(guān)鍵字await。 由于異步函數(shù)采用類(lèi)同步的書(shū)寫(xiě)方法,所以在處理多個(gè)并發(fā)請(qǐng)求,新手可能會(huì)像下面一樣書(shū)寫(xiě)。這樣會(huì)導(dǎo)致url2的請(qǐng)求必需等到url1的請(qǐng)求回來(lái)后才會(huì)發(fā)送。 使用Promise.all的方法來(lái)解決這個(gè)問(wèn)題。Promise.all用于將多個(gè)Promise實(shí)例,包裝成一個(gè)新的 Promis e實(shí)例,當(dāng)所有的 Promise 成功后才會(huì)觸發(fā)Promise.all的resolve函數(shù),當(dāng)有一個(gè)失敗,則立即調(diào)用Promise.all的reject函數(shù)。 目前使用 Babel 已經(jīng)支持 ES7 異步函數(shù)的轉(zhuǎn)碼了,大家可以在自己的項(xiàng)目中開(kāi)始嘗試。 關(guān)于本文 作者:@穿越過(guò)來(lái)的鍵盤(pán)手 |
|
來(lái)自: melon1024 > 《programming》