摘要:調(diào)用棧被清空,消息隊(duì)列中并無(wú)任務(wù),線程停止,事件循環(huán)結(jié)束。不確定的時(shí)間點(diǎn)請(qǐng)求返回,將設(shè)定好的回調(diào)函數(shù)放入消息隊(duì)列。調(diào)用棧執(zhí)行完畢執(zhí)行消息隊(duì)列任務(wù)。請(qǐng)求并發(fā)回調(diào)函數(shù)執(zhí)行順序無(wú)法確定。
異步編程
JavaScript中異步編程問(wèn)題可以說(shuō)是基礎(chǔ)中的重點(diǎn),也是比較難理解的地方。首先要弄懂的是什么叫異步?
我們的代碼在執(zhí)行的時(shí)候是從上到下按順序執(zhí)行,一段代碼執(zhí)行了之后才會(huì)執(zhí)行下一段代碼,這種方式叫同步(synchronous)執(zhí)行,也是我們最容易理解的方式。但是在某些場(chǎng)景下:
網(wǎng)絡(luò)請(qǐng)求:常見(jiàn)的ajax
IO操作:比如readFile
定時(shí)器:setTimeout
上面這些場(chǎng)景可能非常耗時(shí),而且時(shí)間不定長(zhǎng),這時(shí)候這些代碼就不應(yīng)該同步執(zhí)行了,先執(zhí)行可以執(zhí)行的代碼,在未來(lái)的某個(gè)時(shí)間再來(lái)執(zhí)行他們的handler,這就是異步。
通過(guò)這篇文章我們來(lái)了解幾個(gè)知識(shí)點(diǎn):
進(jìn)程線程區(qū)別
消息隊(duì)列與事件循環(huán)
JavaScript處理異步的幾種方法
generator與async/await的關(guān)系
基礎(chǔ)知識(shí)先做些準(zhǔn)備工作,補(bǔ)一補(bǔ)一些非常重要的前置的概念。
進(jìn)程與線程一個(gè)程序(program)至少包含一個(gè)進(jìn)程(process),一個(gè)進(jìn)程至少包含一個(gè)線程(thread)。
進(jìn)程有以下特點(diǎn):
一個(gè)進(jìn)程可以包含一個(gè)或多個(gè)線程。
進(jìn)程在執(zhí)行過(guò)程中擁有獨(dú)立的內(nèi)存單元。
一個(gè)進(jìn)程可以創(chuàng)建和撤銷(xiāo)另一個(gè)進(jìn)程,這個(gè)進(jìn)程是父進(jìn)程,被創(chuàng)建的進(jìn)程稱(chēng)為子進(jìn)程。
線程有以下特點(diǎn):
線程不能獨(dú)立運(yùn)行,必須依賴(lài)進(jìn)程空間。
線程自己基本上不擁有系統(tǒng)資源,只擁有一點(diǎn)在運(yùn)行中必不可少的資源(如程序計(jì)數(shù)器,一組寄存器和棧),但是它可與同屬一個(gè)進(jìn)程的其他的線程共享進(jìn)程所擁有的全部資源。
一個(gè)線程可以創(chuàng)建和撤銷(xiāo)另一個(gè)線程;同一個(gè)進(jìn)程中的多個(gè)線程之間可以并發(fā)執(zhí)行。
從邏輯角度來(lái)看,多線程的意義在于一個(gè)應(yīng)用程序中,有多個(gè)執(zhí)行部分可以同時(shí)執(zhí)行。但操作系統(tǒng)并沒(méi)有將多個(gè)線程看做多個(gè)獨(dú)立的應(yīng)用,來(lái)實(shí)現(xiàn)進(jìn)程的調(diào)度和管理以及資源分配。這就是進(jìn)程和線程的重要區(qū)別。
畫(huà)張圖來(lái)簡(jiǎn)單描述下:
所有的程序都要交給CPU實(shí)現(xiàn)計(jì)算任務(wù),但是CPU一個(gè)時(shí)間點(diǎn)只能處理一個(gè)任務(wù)。這時(shí)如果多個(gè)程序在運(yùn)行,就涉及到了《操作系統(tǒng)原理》中重要的線程調(diào)度算法,線程是CPU輪轉(zhuǎn)的最小單位,其他上下文信息用所在進(jìn)程中的。
進(jìn)程是資源的分配單位,線程是CPU在進(jìn)程內(nèi)切換的單位。JavaScript單線程
瀏覽器內(nèi)核是多線程,在內(nèi)核控制下各線程相互配合以保持同步,一個(gè)瀏覽器通常由以下常駐線程組成:
GUI 渲染線程
JavaScript引擎線程
定時(shí)觸發(fā)器線程
事件觸發(fā)線程
異步http請(qǐng)求線程
Javascript是單線程的,那么為什么Javascript要是單線程的?
這是因?yàn)镴avascript這門(mén)腳本語(yǔ)言誕生的使命所致:JavaScript為處理頁(yè)面中用戶(hù)的交互,以及操作DOM樹(shù)、CSS樣式樹(shù)來(lái)給用戶(hù)呈現(xiàn)一份動(dòng)態(tài)而豐富的交互體驗(yàn)和服務(wù)器邏輯的交互處理。如果JavaScript是多線程的方式來(lái)操作這些UI DOM,則可能出現(xiàn)UI操作的沖突; 如果Javascript是多線程的話,在多線程的交互下,處于UI中的DOM節(jié)點(diǎn)就可能成為一個(gè)臨界資源,假設(shè)存在兩個(gè)線程同時(shí)操作一個(gè)DOM,一個(gè)負(fù)責(zé)修改一個(gè)負(fù)責(zé)刪除,那么這個(gè)時(shí)候就需要瀏覽器來(lái)裁決如何生效哪個(gè)線程的執(zhí)行結(jié)果。當(dāng)然我們可以通過(guò)鎖來(lái)解決上面的問(wèn)題。但為了避免因?yàn)橐肓随i而帶來(lái)更大的復(fù)雜性,Javascript在最初就選擇了單線程執(zhí)行。阻塞和非阻塞
這時(shí)候再理解阻塞非阻塞就好理解了,對(duì)于異步任務(wù),單線程的JavaScript如果什么也不干等待異步任務(wù)結(jié)束,這種狀態(tài)就是阻塞的;如果將異步消息放到一邊,過(guò)會(huì)再處理,就是非阻塞的。
請(qǐng)求不能立即得到應(yīng)答,需要等待,那就是阻塞;否則可以理解為非阻塞。
生活中這種場(chǎng)景太常見(jiàn)了,上廁所排隊(duì)就是阻塞,沒(méi)人直接上就是非阻塞。
事件循環(huán)(event-loop)因?yàn)镴avaScript是單線程的,每個(gè)時(shí)刻都只能一個(gè)事件,所以JavaScript中的同步和異步事件就有了一個(gè)奇妙的執(zhí)行順序。
JavaScript在運(yùn)行時(shí)(runtime)會(huì)產(chǎn)生一個(gè)函數(shù)調(diào)用棧,先入棧的函數(shù)先被執(zhí)行。但是有一些任務(wù)是不需要進(jìn)入調(diào)用棧的,這些任務(wù)被加入到消息隊(duì)列中。當(dāng)函數(shù)調(diào)用棧被清空時(shí)候,就會(huì)執(zhí)行消息隊(duì)列中的任務(wù)(任務(wù)總會(huì)關(guān)聯(lián)一個(gè)函數(shù),并加入到調(diào)用棧),依次執(zhí)行直至所有任務(wù)被清空。由于JavaScript是事件驅(qū)動(dòng),當(dāng)用戶(hù)觸發(fā)事件JavaScript再次運(yùn)行直至清空所有任務(wù),這就是事件循環(huán)。
函數(shù)調(diào)用棧中的任務(wù)永遠(yuǎn)優(yōu)先執(zhí)行,調(diào)用棧無(wú)任務(wù)時(shí)候,遍歷消息隊(duì)列中的任務(wù)。消息隊(duì)列中的任務(wù)關(guān)聯(lián)的函數(shù)(一般就是callback)放入調(diào)用棧中執(zhí)行。
舉兩個(gè)例子:異步請(qǐng)求
function ajax (url, callback){ var req = new XMLHttpRequest(); req.onloadend = callback; req.open("GET", url, true); req.send(); }; console.log(1); ajax("/api/xxxx", function(res){ console.log(res); }); console.log(2);
一個(gè)開(kāi)發(fā)經(jīng)常遇到的業(yè)務(wù)場(chǎng)景,異步請(qǐng)求一個(gè)數(shù)據(jù),上述過(guò)程用圖表示:
圖中三條線分別表示函數(shù)執(zhí)行的調(diào)用棧,異步消息隊(duì)列,以及請(qǐng)求所依賴(lài)的網(wǎng)絡(luò)請(qǐng)求線程(瀏覽器自帶)。執(zhí)行順序:
調(diào)用棧執(zhí)行console.log(1);。
調(diào)用棧執(zhí)行ajax方法,方法里面配置XMLHttpRequest的回調(diào)函數(shù),并交由線程執(zhí)行異步請(qǐng)求。
調(diào)用棧繼續(xù)執(zhí)行console.log(2);。
調(diào)用棧被清空,消息隊(duì)列中并無(wú)任務(wù),JavaScript線程停止,事件循環(huán)結(jié)束。
不確定的時(shí)間點(diǎn)請(qǐng)求返回,將設(shè)定好的回調(diào)函數(shù)放入消息隊(duì)列。
事件循環(huán)再次啟動(dòng),調(diào)用棧中無(wú)函數(shù),執(zhí)行消息隊(duì)列中的任務(wù)function(res){console.log(res);}。
定時(shí)器任務(wù):
console.log(1); setTimeout(function(){ console.log(2); }, 100); setTimeout(function(){ console.log(3); }, 10); console.log(4); // 1 // 4 // 3 // 2
跟上面的例子很像,只不過(guò)異步請(qǐng)求變成了定時(shí)器,上述代碼的指向過(guò)程圖:
執(zhí)行順序如下:
調(diào)用棧執(zhí)行console.log(1);。
執(zhí)行setTimeout向消息隊(duì)列添加一個(gè)定時(shí)器任務(wù)1。
執(zhí)行setTimeout向消息隊(duì)列添加一個(gè)定時(shí)器任務(wù)2。
調(diào)用棧執(zhí)行console.log(4);。
調(diào)用棧執(zhí)行完畢執(zhí)行消息隊(duì)列任務(wù)1。
調(diào)用棧執(zhí)行完畢執(zhí)行消息隊(duì)列任務(wù)2。
消息隊(duì)列任務(wù)2執(zhí)行完畢調(diào)用回調(diào)函數(shù)console.log(3);。
消息隊(duì)列任務(wù)1執(zhí)行完畢調(diào)用回調(diào)函數(shù)console.log(2);。
通過(guò)上面例子可以很好理解,就像工作中你正在做一件事情,這時(shí)候領(lǐng)導(dǎo)給你安排一個(gè)不著急的任務(wù),你停下來(lái)跟領(lǐng)導(dǎo)說(shuō)"等我忙完手里的活就去干",然后把手里的活干完去干領(lǐng)導(dǎo)安排的任務(wù)。所有任務(wù)完成相當(dāng)于完成了一個(gè)事件循環(huán)。
macrotasks 和 microtasksmacrotask 和 microtask 都是屬于上述的異步任務(wù)中的一種,分別是一下 API :
macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick(node), Promises, Object.observe(廢棄), MutationObserver
setTimeout 的 macrotask ,和 Promise 的 microtask 有什么不同呢:
console.log("script start"); setTimeout(function() { console.log("setTimeout"); }, 0); Promise.resolve().then(function() { console.log("promise1"); }).then(function() { console.log("promise2"); }); console.log("script end"); // "script start" // "script end" // "promise1" // "promise2" // "setTimeout"
這里的運(yùn)行結(jié)果是Promise的立即返回的異步任務(wù)會(huì)優(yōu)先于setTimeout延時(shí)為0的任務(wù)執(zhí)行。
原因是任務(wù)隊(duì)列分為 macrotasks 和 microtasks,而Promise中的then方法的函數(shù)會(huì)被推入 microtasks 隊(duì)列,而setTimeout的任務(wù)會(huì)被推入 macrotasks 隊(duì)列。在每一次事件循環(huán)中,macrotask 只會(huì)提取一個(gè)執(zhí)行,而 microtask 會(huì)一直提取,直到 microtasks 隊(duì)列清空。
所以上面實(shí)現(xiàn)循環(huán)的順序:
執(zhí)行函數(shù)調(diào)用棧中的任務(wù)。
函數(shù)調(diào)用棧清空之后,執(zhí)行microtasks隊(duì)列任務(wù)至清空。
執(zhí)行microtask隊(duì)列任務(wù)至清空。
并發(fā)(Concurrency)并發(fā)我們應(yīng)該經(jīng)常聽(tīng)過(guò),跟他類(lèi)似的一個(gè)詞叫并行。
并發(fā):多個(gè)進(jìn)程在一臺(tái)處理機(jī)上同時(shí)運(yùn)行,一個(gè)時(shí)間段內(nèi)處理多件事情,宏觀上好比一個(gè)人邊唱邊跳,微觀上這個(gè)人唱一句跳一步。(可以類(lèi)比時(shí)間片輪轉(zhuǎn)法,多個(gè)線程同時(shí)占用一個(gè)CPU,外部看來(lái)可以并發(fā)處理多個(gè)線程)
并行:多態(tài)擁有相同處理能力的處理機(jī)在同時(shí)處理不同的任務(wù),好比廣場(chǎng)上多個(gè)大媽同時(shí)再調(diào)廣場(chǎng)舞。(多個(gè)CPU同時(shí)處理多個(gè)線程任務(wù))
在JavaScript中,因?yàn)槠涫菃尉€程的原因,所以決定了其每時(shí)刻只能干一件事情,事件循環(huán)是并發(fā)在JavaScript單線程中的一種處理方式。
但是在日常開(kāi)發(fā)中我們肯定見(jiàn)過(guò),同時(shí)發(fā)送多個(gè)請(qǐng)求。這種情況下多個(gè)網(wǎng)絡(luò)線程和js線程共同占用一個(gè)CPU,就是并發(fā)。
異步解決方法雖然已經(jīng)理解了JavaScript中運(yùn)行異步任務(wù)的過(guò)程,但是這樣顯然對(duì)開(kāi)發(fā)不友好,因?yàn)槲覀兺ǔ2⒉恢喇惒饺蝿?wù)在何時(shí)結(jié)束。所以前人開(kāi)發(fā)了多種處理異步的方法。每種方法我們都從三個(gè)角度考慮其優(yōu)缺點(diǎn):
單個(gè)異步寫(xiě)法是否簡(jiǎn)便。
多個(gè)異步按順序執(zhí)行。
多個(gè)異步并發(fā)執(zhí)行。
回調(diào)函數(shù) (callback)一種最常見(jiàn)的處理異步問(wèn)題的方法,將異步任務(wù)結(jié)束時(shí)候要干的事情(回調(diào)函數(shù))作為參數(shù)傳給他,等任務(wù)結(jié)束時(shí)候運(yùn)行回調(diào)函數(shù)。我們常用的$.ajax()和setTimeout都屬于這種方式,但是這樣的問(wèn)題很明顯:多個(gè)異步任務(wù)按順序執(zhí)行非常恐怖。
// 著名的回調(diào)金字塔 asyncEvent1(()=>{ asyncEvent2(()=>{ asyncEvent3(()=>{ asyncEvent4(()=>{ .... }); }); }); });
上面這種情況非常難以維護(hù),在早期Node項(xiàng)目中經(jīng)常出現(xiàn)這種情況,有人對(duì)上面小改動(dòng):
function asyncEvent1CB (){ asyncEvent2(asyncEvent2CB); } function asyncEvent2CB (){ asyncEvent3(asyncEvent3CB); } function asyncEvent3CB (){ asyncEvent4(asyncEvent4CB); } function asyncEvent4CB () { // ... } asyncEvent1(asyncEvent1CB);
這樣講回調(diào)函數(shù)分離出來(lái),邏輯清晰了一些,但是還是很明顯:方法調(diào)用順序是硬編碼,耦合性還是很高。而且一旦同時(shí)發(fā)送多個(gè)請(qǐng)求,這多個(gè)請(qǐng)求的回調(diào)函數(shù)執(zhí)行順序很難保證,維護(hù)起來(lái)非常麻煩。
這就是回調(diào)函數(shù)的弊端:
雖然簡(jiǎn)單,但是不利于閱讀維護(hù)。
多層回調(diào)順序執(zhí)行耦合性很高。
請(qǐng)求并發(fā)回調(diào)函數(shù)執(zhí)行順序無(wú)法確定。
每次只能指定一個(gè)回調(diào)函數(shù),出現(xiàn)錯(cuò)誤程序中斷易崩潰。
雖然回調(diào)函數(shù)這種方式問(wèn)題很多,但是不可否認(rèn)的是在ES6之前,他就是處理異步問(wèn)題普遍較好的方式,而且后面很多方式仍然基于回調(diào)函數(shù)。
事件監(jiān)聽(tīng)(litenter)JavaScript是事件驅(qū)動(dòng),任務(wù)的執(zhí)行不取決代碼的順序,而取決于某一個(gè)事件是否發(fā)生。DOM中有大量事件如onclick,onload,onerror等等。
$(".element1").on("click", function(){ console.log(1); }); $("#element2").on("click", function(){ console.log(2); }); document.getElementById("#element3").addEventListener("click", function(){ console.log(3); }, false);
例如上面這段代碼 你無(wú)法預(yù)知輸出結(jié)果,因?yàn)槭录|發(fā)無(wú)法被預(yù)知。跟這個(gè)很像的還有訂閱者發(fā)布者模式:
github上有個(gè)有意思的小demo。注冊(cè)在發(fā)布者里面的回調(diào)函數(shù)何時(shí)被觸發(fā)取決于發(fā)布者何時(shí)發(fā)布事件,這個(gè)很多時(shí)候也是不可預(yù)知的。
回調(diào)函數(shù)與事件監(jiān)聽(tīng)的區(qū)別:
回調(diào)函數(shù)多是一對(duì)一的關(guān)系,事件監(jiān)聽(tīng)可以是多對(duì)一。
運(yùn)行異步函數(shù),在一個(gè)不確定的時(shí)間段之后運(yùn)行回調(diào)函數(shù);不確定何時(shí)觸發(fā)事件,但是觸發(fā)事件同步響應(yīng)事件的回調(diào)。
事件監(jiān)聽(tīng)相對(duì)于回調(diào)函數(shù),可配置的監(jiān)聽(tīng)(可增可減)關(guān)系減少了耦合性。
不過(guò)事件監(jiān)聽(tīng)也存在問(wèn)題:
多對(duì)多的監(jiān)聽(tīng)組成了一個(gè)復(fù)雜的事件網(wǎng)絡(luò),單個(gè)節(jié)點(diǎn)通常監(jiān)聽(tīng)了多個(gè)事件,維護(hù)成本很大。
多個(gè)異步事件仍然還是回調(diào)的形式。
Promisepromise出場(chǎng)了,當(dāng)年理解promise花了我不少功夫。Promise確實(shí)跟前兩者很不一樣,簡(jiǎn)單說(shuō)下promise。
Promise中文可以翻譯成承諾,現(xiàn)在與未來(lái)的一種關(guān)系,我承諾我會(huì)調(diào)用你的函數(shù)。
Promise三種狀態(tài):pending(進(jìn)行中),fulfilled(已成功),rejected(已失敗),其狀態(tài)只能從進(jìn)行中到成功或者是失敗,不可逆。
已成功和已失敗可以承接不同的回調(diào)函數(shù)。
支持.then鏈?zhǔn)秸{(diào)用,將異步的寫(xiě)法改成同步。
原生支持了race, all等方法,方便適用常見(jiàn)開(kāi)發(fā)場(chǎng)景。
promise更詳細(xì)的內(nèi)容可以看阮一峰老師的文章。
Promise對(duì)于異步處理已經(jīng)十分友好,大多生產(chǎn)環(huán)境已經(jīng)在使用,不過(guò)仍有些缺點(diǎn):
Promise一旦運(yùn)行,不能終止掉。
利用Promise處理一個(gè)異步的后續(xù)處理十分簡(jiǎn)便,但是處理多個(gè)請(qǐng)求按順序執(zhí)行仍然很不方便。
Generator中文翻譯成"生成器",ES6中提供的一種異步編程解決方案,語(yǔ)法行為與傳統(tǒng)函數(shù)完全不同。簡(jiǎn)單來(lái)說(shuō),我可以聲明一個(gè)生成器,生成器可以在執(zhí)行的時(shí)候暫停,交出函數(shù)執(zhí)行權(quán)給其他函數(shù),然后其他函數(shù)可以在需要的時(shí)候讓該函數(shù)再次運(yùn)行。這與之前的JavaScript聽(tīng)起來(lái)完全不同。
詳細(xì)的內(nèi)容參考阮一峰老師的文章,這里我們來(lái)?yè)?jù)幾個(gè)例子,正常的ajax調(diào)用寫(xiě)法看起來(lái)如下:
// 使用setTimeout模擬異步 function ajax (url, cb){ setTimeout(function(){ cb("result"); }, 100); } ajax("/api/a", function(result){ console.log(result); }); // "result"
一旦我們想要多個(gè)異步按順序執(zhí)行,簡(jiǎn)直是噩夢(mèng)。這里使用generator處理異步函數(shù)利用了一個(gè)特點(diǎn):調(diào)用next()函數(shù)就會(huì)繼續(xù)執(zhí)行下去,所以利用這個(gè)特點(diǎn)我們處理異步原理:
將異步邏輯封裝成一個(gè)生成器。
將生成器的異步部分yield出去。
在異步的回調(diào)部分調(diào)用next()將生成器繼續(xù)進(jìn)行下去。
這樣同步,異步,回調(diào)分離,處理異步寫(xiě)起來(lái)非常簡(jiǎn)便。
我們對(duì)上面的例子加以改進(jìn):
// 使用setTimeout模擬異步 function ajax (url, cb){ setTimeout(function(){ cb(url + " result."); }, 100); } function ajaxCallback(result){ console.log(result); it.next(result); } function* ajaxGen (){ var aResult = yield ajax("/api/a", ajaxCallback); console.log("aResult: " + aResult); var bResult = yield ajax("/api/b", ajaxCallback); console.log("bResult: " + bResult); } var it = ajaxGen(); it.next(); // /api/a result. // aResult: /api/a result. // /api/b result. // bResult: /api/b result.
運(yùn)行下上面代碼,可以看到控制臺(tái)輸出結(jié)果居然跟我們書(shū)寫(xiě)的順序一樣!我們稍加改動(dòng):
// 使用setTimeout模擬異步 function ajax (url, cb){ setTimeout(function(){ cb(url + " result."); }, 100); } function run (generator) { var it = generator(ajaxCallback); function ajaxCallback(result){ console.log(result); it.next(result); } it.next(); }; run(function* (cb){ var aResult = yield ajax("/api/a", cb); console.log("aResult: " + aResult); var bResult = yield ajax("/api/b", cb); console.log("bResult: " + bResult); });
簡(jiǎn)單幾下改造便可以生成一個(gè)自執(zhí)行的生成器函數(shù),同時(shí)也完成了異步場(chǎng)景同步化寫(xiě)法。generator的核心在于:同步,異步,回調(diào)三者分離,遇到異步交出函數(shù)執(zhí)行權(quán),再利用回調(diào)控制程序生成器繼續(xù)進(jìn)行。上面的run函數(shù)只是一個(gè)簡(jiǎn)單的實(shí)現(xiàn),業(yè)界已經(jīng)有CO這樣成熟的工具。實(shí)際上開(kāi)發(fā)過(guò)程中通常使用generator搭配Promise實(shí)現(xiàn),再來(lái)修改上面的例子:
// 使用setTimeout模擬異步 function ajax (url){ return new Promise(function(resolve, reject){ setTimeout(function(){ resolve(url + " result."); }, 100); }); } function run (generator) { var it = generator(); function next(result){ var result = it.next(result); if (result.done) return result.value; result.value.then(function(data){ console.log(data); next(data); }); } next(); }; run(function* (){ var aResult = yield ajax("/api/a"); console.log("aResult: " + aResult); var bResult = yield ajax("/api/b"); console.log("bResult: " + bResult); });
使用Promise來(lái)代替callback,理解上花費(fèi)點(diǎn)時(shí)間,大大提高了效率。上面是一種常見(jiàn),之前我用過(guò)generator實(shí)現(xiàn)多張圖片并發(fā)上傳,這種情況下利用generator控制上傳上傳數(shù)量,達(dá)到斷斷續(xù)續(xù)上傳的效果。
進(jìn)化到generator這一步可以說(shuō)是相當(dāng)智能了,無(wú)論是單個(gè)異步,多個(gè)按順序異步,并發(fā)異步處理都十分友好,但是也有幾個(gè)問(wèn)題:
ES6瀏覽器支持問(wèn)題,需要polyfill和babel的支持。
需要借助CO這樣的工具來(lái)完成,流程上理解起來(lái)需要一定時(shí)間。
有沒(méi)有更簡(jiǎn)便的方法?
async/await理解了上面的generator,再來(lái)理解async/await就簡(jiǎn)單多了。
ES2017 標(biāo)準(zhǔn)引入了 async 函數(shù),使得異步操作變得更加方便。async 函數(shù)是什么?一句話,它就是 Generator 函數(shù)的語(yǔ)法糖。
再看一遍上面的例子,然后修改上面的例子用async/await:
// 使用setTimeout模擬異步 function ajax (url){ return new Promise(function(resolve, reject){ setTimeout(function(){ console.log(url + " result."); resolve(url + " result."); }, 100); }); } async function ajaxAsync () { var aResult = await ajax("/api/a"); console.log("aResult: " + aResult); var bResult = await ajax("/api/b"); console.log("bResult: " + bResult); } ajaxAsync();
可以明顯的看到,async/await寫(xiě)法跟generator最后一個(gè)例子很像,基本上就是使用async/await關(guān)鍵字封裝了一個(gè)自執(zhí)行的run方法。
async函數(shù)對(duì) Generator 函數(shù)的改進(jìn),體現(xiàn)在以下四點(diǎn)。
內(nèi)置執(zhí)行器:Generator 函數(shù)的執(zhí)行必須靠執(zhí)行器,所以才有了co模塊,而async函數(shù)自帶執(zhí)行器。也就是說(shuō),async函數(shù)的執(zhí)行,與普通函數(shù)一模一樣,只要一行。
更好的語(yǔ)義:async和await,比起星號(hào)和yield,語(yǔ)義更清楚了。async表示函數(shù)里有異步操作,await表示緊跟在后面的表達(dá)式需要等待結(jié)果。
更廣的適用性:co模塊約定,yield命令后面只能是 Thunk 函數(shù)或 Promise 對(duì)象,而async函數(shù)的await命令后面,可以是 Promise 對(duì)象和原始類(lèi)型的值(數(shù)值、字符串和布爾值,但這時(shí)等同于同步操作)。
返回值是 Promise:async函數(shù)的返回值是 Promise 對(duì)象,這比 Generator 函數(shù)的返回值是 Iterator 對(duì)象方便多了。你可以用then方法指定下一步的操作。
這里async/await不做深入介紹,詳情移步阮一峰老師的博客。
Web worker一個(gè)很不常用的api,但是是一個(gè)異步編程的方法,跟以上幾種又不太一樣。
你可能會(huì)遇到一個(gè)非常耗時(shí)的計(jì)算任務(wù),如果在js線程里運(yùn)行會(huì)造成頁(yè)面卡頓,這時(shí)使用web worker,將計(jì)算任務(wù)丟到里面去,等計(jì)算完成再以事件監(jiān)聽(tīng)的方式通知主線程處理,這是一個(gè)web work的應(yīng)用場(chǎng)景。在這時(shí)候,瀏覽器中是有多個(gè)線程在處理js的,worker同時(shí)可以在創(chuàng)建子線程,實(shí)現(xiàn)js"多線程"。web worker的文檔。實(shí)戰(zhàn)的話看這篇。
與前面幾種方法不同的是,我們絞盡腦汁想把異步事件同步化,但是web worker卻反其道而行,將同步的代碼放到異步的線程中。
目前,web worker通常用于頁(yè)面優(yōu)化的一種手段,使用場(chǎng)景:
使用專(zhuān)用線程進(jìn)行數(shù)學(xué)運(yùn)算:Web Worker最簡(jiǎn)單的應(yīng)用就是用來(lái)做后臺(tái)計(jì)算,而這種計(jì)算并不會(huì)中斷前臺(tái)用戶(hù)的操作。
圖像處理:通過(guò)使用從或者元素中獲取的數(shù)據(jù),可以把圖像分割成幾個(gè)不同的區(qū)域并且把它們推送給并行的不同Workers來(lái)做計(jì)算。
大量數(shù)據(jù)的檢索:當(dāng)需要在調(diào)用 ajax后處理大量的數(shù)據(jù),如果處理這些數(shù)據(jù)所需的時(shí)間長(zhǎng)短非常重要,可以在Web Worker中來(lái)做這些,避免凍結(jié)UI線程。
背景數(shù)據(jù)分析:由于在使用Web Worker的時(shí)候,我們有更多潛在的CPU可用時(shí)間,我們現(xiàn)在可以考慮一下JavaScript中的新應(yīng)用場(chǎng)景。例如,我們可以想像在不影響UI體驗(yàn)的情況下實(shí)時(shí)處理用戶(hù)輸入。利用這樣一種可能,我們可以想像一個(gè)像Word(Office Web Apps 套裝)一樣的應(yīng)用:當(dāng)用戶(hù)打字時(shí)后臺(tái)在詞典中進(jìn)行查找,幫助用戶(hù)自動(dòng)糾錯(cuò)等等。
總結(jié)JavaScript中的異步編程方式目前來(lái)說(shuō)大致這些,其中回調(diào)函數(shù)這種方式是最簡(jiǎn)單最常見(jiàn)的,Promise是目前最受歡迎的方式。前四種方式讓異步編碼模式使我們能夠編寫(xiě)更高效的代碼,而最后一種web worker則讓性能更優(yōu)。這里主要是對(duì)異步編程流程梳理,前提知識(shí)點(diǎn)的補(bǔ)充,而對(duì)于真正的異步編程方式則是以思考分析為主,使用沒(méi)有過(guò)多介紹。最后補(bǔ)充一個(gè)連接:JavaScript異步編程常見(jiàn)面試題,幫助理解。
參考《你所不知道JavaScript》
《JavaScript高級(jí)程序設(shè)計(jì)》
瀏覽器進(jìn)程?線程?傻傻分不清楚!
線程和進(jìn)程的區(qū)別是什么?
并發(fā)模型與事件循環(huán)
理解 JavaScript 中的 macrotask 和 microtask
【轉(zhuǎn)向Javascript系列】深入理解Web Worker
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/95014.html
摘要:作用域分類(lèi)作用域共有兩種主要的工作模型。換句話說(shuō),作用域鏈?zhǔn)腔谡{(diào)用棧的,而不是代碼中的作用域嵌套。詞法作用域詞法作用域中,又可分為全局作用域,函數(shù)作用域和塊級(jí)作用域。 一篇鞏固基礎(chǔ)的文章,也可能是一系列的文章,梳理知識(shí)的遺漏點(diǎn),同時(shí)也探究很多理所當(dāng)然的事情背后的原理。 為什么探究基礎(chǔ)?因?yàn)槟悴蝗ッ嬖嚹憔筒恢阑A(chǔ)有多重要,或者是說(shuō)當(dāng)你的工作經(jīng)歷沒(méi)有亮點(diǎn)的時(shí)候,基礎(chǔ)就是檢驗(yàn)?zāi)愫脡牡囊豁?xiàng)...
摘要:很多高級(jí)編程語(yǔ)言都給新創(chuàng)建的對(duì)象分配一個(gè)引用自身的指針比如中的指針,中的,也有指針,雖然它的指向可能相對(duì)復(fù)雜些,但是指向的,永遠(yuǎn)只可能是對(duì)象。 很多高級(jí)編程語(yǔ)言都給新創(chuàng)建的對(duì)象分配一個(gè)引用自身的指針,比如JAVA、C++中的this指針,python中的self,JavaScript也有this指針,雖然它的指向可能相對(duì)復(fù)雜些,但是this指向的,永遠(yuǎn)只可能是對(duì)象。 一、在一般函數(shù)方法...
摘要:很多高級(jí)編程語(yǔ)言都給新創(chuàng)建的對(duì)象分配一個(gè)引用自身的指針比如中的指針,中的,也有指針,雖然它的指向可能相對(duì)復(fù)雜些,但是指向的,永遠(yuǎn)只可能是對(duì)象。 很多高級(jí)編程語(yǔ)言都給新創(chuàng)建的對(duì)象分配一個(gè)引用自身的指針,比如JAVA、C++中的this指針,python中的self,JavaScript也有this指針,雖然它的指向可能相對(duì)復(fù)雜些,但是this指向的,永遠(yuǎn)只可能是對(duì)象。 一、在一般函數(shù)方法...
摘要:程序員的入門(mén)規(guī)劃我該學(xué)習(xí)什么語(yǔ)言這個(gè)問(wèn)題困擾了幾乎所有的程序員,比如應(yīng)用廣好就業(yè),比如入門(mén)簡(jiǎn)單,和安卓待遇高,和開(kāi)發(fā)效率高,是萬(wàn)能語(yǔ)言,和前端缺人才等等個(gè)人見(jiàn)解先學(xué)習(xí)難度小,大眾化的編程語(yǔ)言,比如,,,這幾個(gè)學(xué)哪一種其實(shí)差不多,入門(mén)以后看自 程序員的入門(mén)規(guī)劃 1.我該學(xué)習(xí)什么語(yǔ)言? 這個(gè)問(wèn)題困擾了幾乎所有的程序員,比如java應(yīng)用廣好就業(yè),比如php入門(mén)簡(jiǎn)單,ios和安卓待遇高,rub...
閱讀 2989·2021-11-23 09:51
閱讀 3007·2021-11-02 14:46
閱讀 870·2021-11-02 14:45
閱讀 2751·2021-09-23 11:57
閱讀 2501·2021-09-23 11:22
閱讀 1931·2019-08-29 16:29
閱讀 749·2019-08-29 16:16
閱讀 947·2019-08-26 13:44