JavaScript異步編程
還記得一年前寫過(guò)一篇關(guān)于 JavaScript異步編程簡(jiǎn)述 的文章,主要介紹了JavaScript的單線程特性與異步編程實(shí)現(xiàn)方式:
回調(diào)函數(shù),發(fā)布訂閱模式,Promise對(duì)象三種,關(guān)于Promise介紹的比較簡(jiǎn)略,決定再詳細(xì)總結(jié)一下,既是對(duì)上一篇文章的補(bǔ)充,也能以更深刻的方式分享自己關(guān)于異步編程的理解。
前言如果你有志于成為一個(gè)優(yōu)秀的前端工程師,或是想要深入學(xué)習(xí)JavaScript,異步編程是必不可少的一個(gè)知識(shí)點(diǎn),這也是區(qū)分初級(jí),中級(jí)或高級(jí)前端的依據(jù)之一。如果你對(duì)異步編程沒(méi)有太清晰的概念,那么我建議你花點(diǎn)時(shí)間學(xué)習(xí)JavaScript異步編程,如果你對(duì)異步編程有自己的獨(dú)特理解,也歡迎閱讀本文,一起交流。
同步與異步介紹異步之前,回顧一下,所謂同步編程,就是計(jì)算機(jī)一行一行按順序依次執(zhí)行代碼,當(dāng)前代碼任務(wù)耗時(shí)執(zhí)行會(huì)阻塞后續(xù)代碼的執(zhí)行。
同步編程,即是一種典型的請(qǐng)求-響應(yīng)模型,當(dāng)請(qǐng)求調(diào)用一個(gè)函數(shù)或方法后,需等待其響應(yīng)返回,然后執(zhí)行后續(xù)代碼。
一般情況下,同步編程,代碼按序依次執(zhí)行,能很好的保證程序的執(zhí)行,但是在某些場(chǎng)景下,比如讀取文件內(nèi)容,或請(qǐng)求服務(wù)器接口數(shù)據(jù),需要根據(jù)返回的數(shù)據(jù)內(nèi)容執(zhí)行后續(xù)操作,讀取文件和請(qǐng)求接口直到數(shù)據(jù)返回這一過(guò)程是需要時(shí)間的,網(wǎng)絡(luò)越差,耗費(fèi)時(shí)間越長(zhǎng),如果按照同步編程方式實(shí)現(xiàn),在等待數(shù)據(jù)返回這段時(shí)間,JavaScript是不能處理其他任務(wù)的,此時(shí)頁(yè)面的交互,滾動(dòng)等任何操作也都會(huì)被阻塞,這顯然是及其不友好,不可接受的,而這正是需要異步編程大顯身手的場(chǎng)景,如下圖,耗時(shí)任務(wù)A會(huì)阻塞任務(wù)B的執(zhí)行,等到任務(wù)A執(zhí)行完才能繼續(xù)執(zhí)行B:
當(dāng)使用異步編程時(shí),在等待當(dāng)前任務(wù)的響應(yīng)返回之前,可以繼續(xù)執(zhí)行后續(xù)代碼,即當(dāng)前執(zhí)行任務(wù)不會(huì)阻塞后續(xù)執(zhí)行。
異步編程,不同于同步編程的請(qǐng)求-響應(yīng)模式,其是一種 事件驅(qū)動(dòng)編程 ,請(qǐng)求調(diào)用函數(shù)或方法后,無(wú)需立即等待響應(yīng),可以繼續(xù)執(zhí)行其他任務(wù),而之前任務(wù)響應(yīng)返回后可以通過(guò)狀態(tài)、通知和回調(diào)來(lái)通知調(diào)用者。
多線程
前面說(shuō)明了異步編程能很好的解決同步編程阻塞的問(wèn)題,那么實(shí)現(xiàn)異步的方式有哪些呢?通常實(shí)現(xiàn)異步方式是多線程,如C#, 即同時(shí)開(kāi)啟多個(gè)線程,不同操作能并行執(zhí)行,如下圖,耗時(shí)任務(wù)A執(zhí)行的同時(shí),在線程二中任務(wù)B也可以執(zhí)行:
JavaScript單線程
JavaScript語(yǔ)言執(zhí)行環(huán)境是單線程的,單線程在程序執(zhí)行時(shí),所走的程序路徑按照連續(xù)順序排下來(lái),前面的必須處理好,后面的才會(huì)執(zhí)行,而使用異步實(shí)現(xiàn)時(shí),多個(gè)任務(wù)可以并發(fā)執(zhí)行。那么JavaScript的異步編程如何實(shí)現(xiàn)呢,下一節(jié)將詳細(xì)闡述其異步機(jī)制。
并行與并發(fā)
前文提到多線程的任務(wù)可以并行執(zhí)行,而JavaScript單線程異步編程可以實(shí)現(xiàn)多任務(wù)并發(fā)執(zhí)行,這里有必要說(shuō)明一下并行與并發(fā)的區(qū)別。
并行,指同一時(shí)刻內(nèi)多任務(wù)同時(shí)進(jìn)行; 并發(fā),指在同一時(shí)間段內(nèi),多任務(wù)同時(shí)進(jìn)行著,但是某一時(shí)刻,只有某一任務(wù)執(zhí)行;通常所說(shuō)的并發(fā)連接數(shù),是指瀏覽器向服務(wù)器發(fā)起請(qǐng)求,建立TCP連接,每秒鐘服務(wù)器建立的總連接數(shù),而假如,服務(wù)器處10ms能處理一個(gè)連接,那么其并發(fā)連接數(shù)就是100。
JavaScript異步機(jī)制本節(jié)介紹JavaScript異步機(jī)制,首先來(lái)看一個(gè)例子:
for (var i = 0; i < 5; i ++) {setTimeout(function(){ console.log(i);}, 0); } console.log(i); //5 ; 5 ; 5 ; 5; 5
應(yīng)該明白最后輸出的全是5:
i在此處是for循環(huán)所在上下文環(huán)境的變量,有且只有一個(gè)i; 循環(huán)結(jié)束時(shí)i==5; JavaScript單線程事件處理器在線程空閑前不會(huì)執(zhí)行下一事件。如上面第三點(diǎn)所述,如果要真正理解以上例子中的setTimeout(),及JavaScript異步機(jī)制,需要理解JavaScript的事件循環(huán)和并發(fā)模型。
并發(fā)模型(Concurrency model)目前,我們已經(jīng)知道,JavaScript執(zhí)行異步任務(wù)時(shí),不需要等待響應(yīng)返回,可以繼續(xù)執(zhí)行其他任務(wù),而在響應(yīng)返回時(shí),會(huì)得到通知,執(zhí)行回調(diào)或事件處理程序。那么這一切具體是如何完成的,又以什么規(guī)則或順序運(yùn)作呢?接下來(lái)我們需要解答這個(gè)問(wèn)題。
注:回調(diào)和事件處理程序本質(zhì)上并無(wú)區(qū)別,只是在不同情況下,不同的叫法。
前文已經(jīng)提到,JavaScript異步編程使得多個(gè)任務(wù)可以并發(fā)執(zhí)行,而實(shí)現(xiàn)這一功能的基礎(chǔ)是JavScript擁有一個(gè)基于事件循環(huán)的并發(fā)模型。
堆棧與隊(duì)列
介紹JavaScript并發(fā)模型之前,先簡(jiǎn)單介紹堆棧和隊(duì)列的區(qū)別:
堆(heap):內(nèi)存中某一未被阻止的區(qū)域,通常存儲(chǔ)對(duì)象(引用類型); 棧(stack):后進(jìn)先出的順序存儲(chǔ)數(shù)據(jù)結(jié)構(gòu),通常存儲(chǔ)函數(shù)參數(shù)和基本類型值變量(按值訪問(wèn)); 隊(duì)列(queue):先進(jìn)先出順序存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)。 事件循環(huán)(Event Loop)JavaScript引擎負(fù)責(zé)解析,執(zhí)行JavaScript代碼,但它并不能單獨(dú)運(yùn)行,通常都得有一個(gè)宿主環(huán)境,一般如瀏覽器或Node服務(wù)器,前文說(shuō)到的單線程是指在這些宿主環(huán)境創(chuàng)建單一線程,提供一種機(jī)制,調(diào)用JavaScript引擎完成多個(gè)JavaScript代碼塊的調(diào)度,執(zhí)行(是的,JavaScript代碼都是按塊執(zhí)行的),這種機(jī)制就稱為事件循環(huán)(Event Loop)。
注:這里的事件與DOM事件不要混淆,可以說(shuō)這里的事件包括DOM事件,所有異步操作都是一個(gè)事件,諸如ajax請(qǐng)求就可以看作一個(gè)request請(qǐng)求事件。
JavaScript執(zhí)行環(huán)境中存在的兩個(gè)結(jié)構(gòu)需要了解:
消息隊(duì)列(message queue),也叫任務(wù)隊(duì)列(task queue):存儲(chǔ)待處理消息及對(duì)應(yīng)的回調(diào)函數(shù)或事件處理程序; 執(zhí)行棧(execution context stack),也可以叫執(zhí)行上下文棧:JavaScript執(zhí)行棧,顧名思義,是由執(zhí)行上下文組成,當(dāng)函數(shù)調(diào)用時(shí),創(chuàng)建并插入一個(gè)執(zhí)行上下文,通常稱為執(zhí)行棧幀(frame),存儲(chǔ)著函數(shù)參數(shù)和局部變量,當(dāng)該函數(shù)執(zhí)行結(jié)束時(shí),彈出該執(zhí)行棧幀;注:關(guān)于全局代碼,由于所有的代碼都是在全局上下文執(zhí)行,所以執(zhí)行棧頂總是全局上下文就很容易理解,直到所有代碼執(zhí)行完畢,全局上下文退出執(zhí)行棧,棧清空了;也即是全局上下文是第一個(gè)入棧,最后一個(gè)出棧。
任務(wù)
分析事件循環(huán)流程前,先闡述兩個(gè)概念,有助于理解事件循環(huán):同步任務(wù)和異步任務(wù)。
任務(wù)很好理解,JavaScript代碼執(zhí)行就是在完成任務(wù),所謂任務(wù)就是一個(gè)函數(shù)或一個(gè)代碼塊,通常以功能或目的劃分,比如完成一次加法計(jì)算,完成一次ajax請(qǐng)求;很自然的就分為同步任務(wù)和異步任務(wù)。同步任務(wù)是連續(xù)的,阻塞的;而異步任務(wù)則是不連續(xù),非阻塞的,包含異步事件及其回調(diào),當(dāng)我們談及執(zhí)行異步任務(wù)時(shí),通常指執(zhí)行其回調(diào)函數(shù)。
事件循環(huán)流程
關(guān)于事件循環(huán)流程分解如下:
宿主環(huán)境為JavaScript創(chuàng)建線程時(shí),會(huì)創(chuàng)建堆(heap)和棧(stack),堆內(nèi)存儲(chǔ)JavaScript對(duì)象,棧內(nèi)存儲(chǔ)執(zhí)行上下文; 棧內(nèi)執(zhí)行上下文的同步任務(wù)按序執(zhí)行,執(zhí)行完即退棧,而當(dāng)異步任務(wù)執(zhí)行時(shí),該異步任務(wù)進(jìn)入等待狀態(tài)(不入棧),同時(shí)通知線程:當(dāng)觸發(fā)該事件時(shí)(或該異步操作響應(yīng)返回時(shí)),需向消息隊(duì)列插入一個(gè)事件消息; 當(dāng)事件觸發(fā)或響應(yīng)返回時(shí),線程向消息隊(duì)列插入該事件消息(包含事件及回調(diào)); 當(dāng)棧內(nèi)同步任務(wù)執(zhí)行完畢后,線程從消息隊(duì)列取出一個(gè)事件消息,其對(duì)應(yīng)異步任務(wù)(函數(shù))入棧,執(zhí)行回調(diào)函數(shù),如果未綁定回調(diào),這個(gè)消息會(huì)被丟棄,執(zhí)行完任務(wù)后退棧; 當(dāng)線程空閑(即執(zhí)行棧清空)時(shí)繼續(xù)拉取消息隊(duì)列下一輪消息(next tick,事件循環(huán)流轉(zhuǎn)一次稱為一次tick)。使用代碼可以描述如下:
var eventLoop = []; var event; var i = eventLoop.length - 1; // 后進(jìn)先出 while(eventLoop[i]) {event = eventLoop[i--]; if (event) { // 事件回調(diào)存在 event();}// 否則事件消息被丟棄 }
這里注意的一點(diǎn)是等待下一個(gè)事件消息的過(guò)程是同步的。
并發(fā)模型與事件循環(huán)
var ele = document.querySelector(’body’); function clickCb(event) {console.log(’clicked’); } function bindEvent(callback) {ele.addEventListener(’click’, callback); } bindEvent(clickCb);
針對(duì)如上代碼我們可以構(gòu)建如下并發(fā)模型:
如上圖,當(dāng)執(zhí)行棧同步代碼塊依次執(zhí)行完直到遇見(jiàn)異步任務(wù)時(shí),異步任務(wù)進(jìn)入等待狀態(tài),通知線程,異步事件觸發(fā)時(shí),往消息隊(duì)列插入一條事件消息;而當(dāng)執(zhí)行棧后續(xù)同步代碼執(zhí)行完后,讀取消息隊(duì)列,得到一條消息,然后將該消息對(duì)應(yīng)的異步任務(wù)入棧,執(zhí)行回調(diào)函數(shù);一次事件循環(huán)就完成了,也即處理了一個(gè)異步任務(wù)。
再談setTimeout(…0)
了解了JavaScript事件循環(huán)后我們?cè)倏辞拔年P(guān)于 setTimeout(...0) 的例子就比較清晰了:
setTimeout(...0) 所表達(dá)的意思是:等待0秒后(這個(gè)時(shí)間由第二個(gè)參數(shù)值確定),往消息隊(duì)列插入一條定時(shí)器事件消息,并將其第一個(gè)參數(shù)作為回調(diào)函數(shù);而當(dāng)執(zhí)行棧內(nèi)同步任務(wù)執(zhí)行完畢時(shí),線程從消息隊(duì)列讀取消息,將該異步任務(wù)入棧,執(zhí)行;線程空閑時(shí)再次從消息隊(duì)列讀取消息。
再看一個(gè)實(shí)例:
var start = +new Date(); var arr = []; setTimeout(function(){console.log(’time: ’ + (new Date().getTime() - start)); },10); for(var i=0;i<=1000000;i++){arr.push(i); }
執(zhí)行多次輸出如下:
在 setTimeout 異步回調(diào)函數(shù)里我們輸出了異步任務(wù)注冊(cè)到執(zhí)行的時(shí)間,發(fā)現(xiàn)并不等于我們指定的時(shí)間,而且兩次時(shí)間間隔也都不同,考慮以下兩點(diǎn):
在讀取消息隊(duì)列的消息時(shí),得等同步任務(wù)完成,這個(gè)是需要耗費(fèi)時(shí)間的; 消息隊(duì)列先進(jìn)先出原則,讀取此異步事件消息之前,可能還存在其他消息,執(zhí)行也需要耗時(shí);所以異步執(zhí)行時(shí)間不精確是必然的,所以我們有必要明白無(wú)論是同步任務(wù)還是異步任務(wù),都不應(yīng)該耗時(shí)太長(zhǎng),當(dāng)一個(gè)消息耗時(shí)太長(zhǎng)時(shí),應(yīng)該盡可能的將其分割成多個(gè)消息。
Web Workers
每個(gè)Web Worker或一個(gè)跨域的iframe都有各自的堆棧和消息隊(duì)列,這些不同的文檔只能通過(guò)postMessage方法進(jìn)行通信,當(dāng)一方監(jiān)聽(tīng)了message事件后,另一方才能通過(guò)該方法向其發(fā)送消息,這個(gè)message事件也是異步的,當(dāng)一方接收到另一方通過(guò)postMessage方法發(fā)送來(lái)的消息后,會(huì)向自己的消息隊(duì)列插入一條消息,而后續(xù)的并發(fā)流程依然如上文所述。
JavaScript異步實(shí)現(xiàn)關(guān)于JavaScript的異步實(shí)現(xiàn),以前有:回調(diào)函數(shù),發(fā)布訂閱模式,Promise三類,而在ES6中提出了生成器(Generator)方式實(shí)現(xiàn),關(guān)于回調(diào)函數(shù)和發(fā)布訂閱模式實(shí)現(xiàn)可參見(jiàn)另一篇文章,后續(xù)將推出一篇詳細(xì)介紹Promise和Generator。
參考:
Concurrency model and Event Loop
來(lái)自:http://blog.codingplayboy.com/2017/04/25/js_async/
相關(guān)文章:
1. IntelliJ IDEA設(shè)置默認(rèn)瀏覽器的方法2. SpringBoot項(xiàng)目?jī)?yōu)雅的全局異常處理方式(全網(wǎng)最新)3. Python TestSuite生成測(cè)試報(bào)告過(guò)程解析4. python操作數(shù)據(jù)庫(kù)獲取結(jié)果之fetchone和fetchall的區(qū)別說(shuō)明5. IntelliJ IDEA設(shè)置背景圖片的方法步驟6. 解決python路徑錯(cuò)誤,運(yùn)行.py文件,找不到路徑的問(wèn)題7. docker /var/lib/docker/aufs/mnt 目錄清理方法8. 在JSP中使用formatNumber控制要顯示的小數(shù)位數(shù)方法9. 如何清空python的變量10. Xml簡(jiǎn)介_(kāi)動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
