還記得一年前寫過一篇關(guān)于JavaScript異步編程簡述的文章契耿,主要介紹了JavaScript的單線程特性與異步編程實現(xiàn)方式:回調(diào)函數(shù)瞒大,發(fā)布訂閱模式,Promise對象三種搪桂,關(guān)于Promise介紹的比較簡略透敌,決定再詳細(xì)總結(jié)一下,既是對上一篇文章的補充踢械,也能以更深刻的方式分享自己關(guān)于異步編程的理解酗电。
前言
如果你有志于成為一個優(yōu)秀的前端工程師,或是想要深入學(xué)習(xí)JavaScript裸燎,異步編程是必不可少的一個知識點顾瞻,這也是區(qū)分初級,中級或高級前端的依據(jù)之一德绿。如果你對異步編程沒有太清晰的概念荷荤,那么我建議你花點時間學(xué)習(xí)JavaScript異步編程,如果你對異步編程有自己的獨特理解移稳,也歡迎閱讀本文蕴纳,一起交流。
同步與異步
介紹異步之前个粱,回顧一下古毛,所謂同步編程,就是計算機一行一行按順序依次執(zhí)行代碼都许,當(dāng)前代碼任務(wù)耗時執(zhí)行會阻塞后續(xù)代碼的執(zhí)行稻薇。
同步編程,即是一種典型的請求-響應(yīng)模型胶征,當(dāng)請求調(diào)用一個函數(shù)或方法后塞椎,需等待其響應(yīng)返回,然后執(zhí)行后續(xù)代碼睛低。
一般情況下案狠,同步編程,代碼按序依次執(zhí)行钱雷,能很好的保證程序的執(zhí)行骂铁,但是在某些場景下,比如讀取文件內(nèi)容罩抗,或請求服務(wù)器接口數(shù)據(jù)拉庵,需要根據(jù)返回的數(shù)據(jù)內(nèi)容執(zhí)行后續(xù)操作,讀取文件和請求接口直到數(shù)據(jù)返回這一過程是需要時間的套蒂,網(wǎng)絡(luò)越差名段,耗費時間越長阱扬,如果按照同步編程方式實現(xiàn),在等待數(shù)據(jù)返回這段時間伸辟,JavaScript是不能處理其他任務(wù)的麻惶,此時頁面的交互,滾動等任何操作也都會被阻塞信夫,這顯然是及其不友好窃蹋,不可接受的,而這正是需要異步編程大顯身手的場景静稻,如下圖警没,耗時任務(wù)A會阻塞任務(wù)B的執(zhí)行,等到任務(wù)A執(zhí)行完才能繼續(xù)執(zhí)行B:
當(dāng)使用異步編程時振湾,在等待當(dāng)前任務(wù)的響應(yīng)返回之前杀迹,可以繼續(xù)執(zhí)行后續(xù)代碼,即當(dāng)前執(zhí)行任務(wù)不會阻塞后續(xù)執(zhí)行押搪。
異步編程树酪,不同于同步編程的請求-響應(yīng)模式,其是一種事件驅(qū)動編程大州,請求調(diào)用函數(shù)或方法后续语,無需立即等待響應(yīng),可以繼續(xù)執(zhí)行其他任務(wù)厦画,而之前任務(wù)響應(yīng)返回后可以通過狀態(tài)疮茄、通知和回調(diào)來通知調(diào)用者。
多線程
前面說明了異步編程能很好的解決同步編程阻塞的問題根暑,那么實現(xiàn)異步的方式有哪些呢力试?通常實現(xiàn)異步方式是多線程,如C#, 即同時開啟多個線程排嫌,不同操作能并行執(zhí)行畸裳,如下圖,耗時任務(wù)A執(zhí)行的同時躏率,在線程二中任務(wù)B也可以執(zhí)行:
JAVASCRIPT單線程
JavaScript語言執(zhí)行環(huán)境是單線程的,單線程在程序執(zhí)行時民鼓,所走的程序路徑按照連續(xù)順序排下來薇芝,前面的必須處理好,后面的才會執(zhí)行丰嘉,而使用異步實現(xiàn)時夯到,多個任務(wù)可以并發(fā)執(zhí)行。那么JavaScript的異步編程如何實現(xiàn)呢饮亏,下一節(jié)將詳細(xì)闡述其異步機制耍贾。
并行與并發(fā)
前文提到多線程的任務(wù)可以并行執(zhí)行阅爽,而JavaScript單線程異步編程可以實現(xiàn)多任務(wù)并發(fā)執(zhí)行,這里有必要說明一下并行與并發(fā)的區(qū)別荐开。
- 并行付翁,指同一時刻內(nèi)多任務(wù)同時進(jìn)行;
- 并發(fā)晃听,指在同一時間段內(nèi)百侧,多任務(wù)同時進(jìn)行著,但是某一時刻只有某一任務(wù)執(zhí)行
通常所說的并發(fā)連接數(shù)能扒,是指瀏覽器向服務(wù)器發(fā)起請求佣渴,建立TCP連接,每秒鐘服務(wù)器建立的總連接數(shù)初斑,而假如辛润,服務(wù)器處10ms能處理一個連接,那么其并發(fā)連接數(shù)就是100见秤。
JavaScript異步機制
本節(jié)介紹JavaScript異步機制砂竖,首先來看一個例子:
for (var i = 0; i < 5; i ++) {
setTimeout(function(){
console.log(i);
}, 0);
}
console.log(i);
//5 ; 5 ; 5 ; 5 ; 5 ; 5
應(yīng)該明白最后輸出的全是5:
- i在此處是for循環(huán)所在上下文環(huán)境的變量,有且只有一個i;
- 循環(huán)結(jié)束時i==5;
- JavaScript單線程事件處理器在線程空閑前不會執(zhí)行下一事件秦叛。
如上面第三點所述晦溪,如果要真正理解以上例子中的setTimeout(),及JavaScript異步機制挣跋,需要理解JavaScript的事件循環(huán)和并發(fā)模型三圆。
并發(fā)模型(Concurrency model)
目前,我們已經(jīng)知道避咆,JavaScript執(zhí)行異步任務(wù)時舟肉,不需要等待響應(yīng)返回,可以繼續(xù)執(zhí)行其他任務(wù)查库,而在響應(yīng)返回時路媚,會得到通知,執(zhí)行回調(diào)或事件處理程序樊销。那么這一切具體是如何完成的整慎,又以什么規(guī)則或順序運作呢?接下來我們需要解答這個問題围苫。
注:回調(diào)和事件處理程序本質(zhì)上并無區(qū)別裤园,只是在不同情況下,不同的叫法剂府。
前文已經(jīng)提到拧揽,JavaScript異步編程使得多個任務(wù)可以并發(fā)執(zhí)行,而實現(xiàn)這一功能的基礎(chǔ)是JavScript擁有一個基于事件循環(huán)的并發(fā)模型。
堆棧與隊列
介紹JavaScript并發(fā)模型之前淤袜,先簡單介紹堆棧和隊列的區(qū)別:
- 堆(heap):內(nèi)存中某一未被阻止的區(qū)域痒谴,通常存儲對象(引用類型);
- 棧(stack):后進(jìn)先出的順序存儲數(shù)據(jù)結(jié)構(gòu)铡羡,通常存儲函數(shù)參數(shù)和基本類型值變量(按值訪問)积蔚;
- 隊列(queue):先進(jìn)先出順序存儲數(shù)據(jù)結(jié)構(gòu)。
事件循環(huán)(Event Loop)
JavaScript引擎負(fù)責(zé)解析蓖墅,執(zhí)行JavaScript代碼库倘,但它并不能單獨運行,通常都得有一個宿主環(huán)境论矾,一般如瀏覽器或Node服務(wù)器教翩,前文說到的單線程是指在這些宿主環(huán)境創(chuàng)建單一線程,提供一種機制贪壳,調(diào)用JavaScript引擎完成多個JavaScript代碼塊的調(diào)度饱亿,執(zhí)行(是的,JavaScript代碼都是按塊執(zhí)行的)闰靴,這種機制就稱為事件循環(huán)(Event Loop)彪笼。
JavaScript執(zhí)行環(huán)境中存在的兩個結(jié)構(gòu)需要了解:
消息隊列(message queue)衬以,也叫任務(wù)隊列(task queue):存儲待處理消息及對應(yīng)的回調(diào)函數(shù)或事件處理程序固逗;
執(zhí)行棧(execution context stack)庐冯,也可以叫執(zhí)行上下文棧:JavaScript執(zhí)行棧系吩,顧名思義,是由執(zhí)行上下文組成未蝌,當(dāng)函數(shù)調(diào)用時光督,創(chuàng)建并插入一個執(zhí)行上下文焕蹄,通常稱為執(zhí)行棧幀(frame)淑翼,存儲著函數(shù)參數(shù)和局部變量腐巢,當(dāng)該函數(shù)執(zhí)行結(jié)束時,彈出該執(zhí)行棧幀玄括;
注:關(guān)于全局代碼冯丙,由于所有的代碼都是在全局上下文執(zhí)行,所以執(zhí)行棧頂總是全局上下文就很容易理解遭京,直到所有代碼執(zhí)行完畢胃惜,全局上下文退出執(zhí)行棧,棧清空了哪雕;也即是全局上下文是第一個入棧船殉,最后一個出棧。
任務(wù)
分析事件循環(huán)流程前热监,先闡述兩個概念捺弦,有助于理解事件循環(huán):同步任務(wù)和異步任務(wù)饮寞。
任務(wù)很好理解孝扛,JavaScript代碼執(zhí)行就是在完成任務(wù)列吼,所謂任務(wù)就是一個函數(shù)或一個代碼塊,通常以功能或目的劃分苦始,比如完成一次加法計算寞钥,完成一次ajax請求;很自然的就分為同步任務(wù)和異步任務(wù)陌选。同步任務(wù)是連續(xù)的理郑,阻塞的;而異步任務(wù)則是不連續(xù)咨油,非阻塞的您炉,包含異步事件及其回調(diào),當(dāng)我們談及執(zhí)行異步任務(wù)時役电,通常指執(zhí)行其回調(diào)函數(shù)赚爵。
事件循環(huán)流程
關(guān)于事件循環(huán)流程分解如下:
- 宿主環(huán)境為JavaScript創(chuàng)建線程時,會創(chuàng)建堆(heap)和棧(stack)法瑟,堆內(nèi)存儲JavaScript對象冀膝,棧內(nèi)存儲執(zhí)行上下文;
- 棧內(nèi)執(zhí)行上下文的同步任務(wù)按序執(zhí)行霎挟,執(zhí)行完即退棧窝剖,而當(dāng)異步任務(wù)執(zhí)行時,該異步任務(wù)進(jìn)入等待狀態(tài)(不入棧)酥夭,同時通知線程:當(dāng)觸發(fā)該事件時(或該異步操作響應(yīng)返回時)赐纱,需向消息隊列插入一個事件消息;
- 當(dāng)事件觸發(fā)或響應(yīng)返回時采郎,線程向消息隊列插入該事件消息(包含事件及回調(diào))千所;
- 當(dāng)棧內(nèi)同步任務(wù)執(zhí)行完畢后,線程從消息隊列取出一個事件消息蒜埋,其對應(yīng)異步任務(wù)(函數(shù))入棧淫痰,執(zhí)行回調(diào)函數(shù),如果未綁定回調(diào)整份,這個消息會被丟棄待错,執(zhí)行完任務(wù)后退棧;
- 當(dāng)線程空閑(即執(zhí)行棧清空)時繼續(xù)拉取消息隊列下一輪消息(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();
}
// 否則事件消息被丟棄
}
這里注意的一點是等待下一個事件消息的過程是同步的。
并發(fā)模型與事件循環(huán)
var ele = document.querySelector('body');
function clickCb(event) {
console.log('clicked');
}
function bindEvent(callback) {
ele.addEventListener('click', callback);
}
bindEvent(clickCb);
針對如上代碼我們可以構(gòu)建如下并發(fā)模型:
如上圖讲冠,當(dāng)執(zhí)行棧同步代碼塊依次執(zhí)行完直到遇見異步任務(wù)時瓜客,異步任務(wù)進(jìn)入等待狀態(tài),通知線程,異步事件觸發(fā)時谱仪,往消息隊列插入一條事件消息玻熙;而當(dāng)執(zhí)行棧后續(xù)同步代碼執(zhí)行完后,讀取消息隊列疯攒,得到一條消息嗦随,然后將該消息對應(yīng)的異步任務(wù)入棧,執(zhí)行回調(diào)函數(shù)敬尺;一次事件循環(huán)就完成了枚尼,也即處理了一個異步任務(wù)。
再談SETTIMEOUT(…0)
了解了JavaScript事件循環(huán)后我們再看前文關(guān)于setTimeout(...0)的例子就比較清晰了:
setTimeout(...0)所表達(dá)的意思是:等待0秒后(這個時間由第二個參數(shù)值確定)砂吞,往消息隊列插入一條定時器事件消息署恍,并將其第一個參數(shù)作為回調(diào)函數(shù);而當(dāng)執(zhí)行棧內(nèi)同步任務(wù)執(zhí)行完畢時蜻直,線程從消息隊列讀取消息锭汛,將該異步任務(wù)入棧,執(zhí)行袭蝗;線程空閑時再次從消息隊列讀取消息唤殴。
再看一個實例:
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ù)注冊到執(zhí)行的時間,發(fā)現(xiàn)并不等于我們指定的時間到腥,而且兩次時間間隔也都不同朵逝,考慮以下兩點:
- 在讀取消息隊列的消息時,得等同步任務(wù)完成乡范,這個是需要耗費時間的配名;
- 消息隊列先進(jìn)先出原則,讀取此異步事件消息之前晋辆,可能還存在其他消息渠脉,執(zhí)行也需要耗時;
所以異步執(zhí)行時間不精確是必然的瓶佳,所以我們有必要明白無論是同步任務(wù)還是異步任務(wù)芋膘,都不應(yīng)該耗時太長,當(dāng)一個消息耗時太長時霸饲,應(yīng)該盡可能的將其分割成多個消息为朋。
WEB WORKERS
每個Web Worker或一個跨域的iframe都有各自的堆棧和消息隊列,這些不同的文檔只能通過postMessage方法進(jìn)行通信厚脉,當(dāng)一方監(jiān)聽了message事件后习寸,另一方才能通過該方法向其發(fā)送消息,這個message事件也是異步的傻工,當(dāng)一方接收到另一方通過postMessage方法發(fā)送來的消息后霞溪,會向自己的消息隊列插入一條消息孵滞,而后續(xù)的并發(fā)流程依然如上文所述。
JavaScript異步實現(xiàn)
關(guān)于JavaScript的異步實現(xiàn)鸯匹,以前有:回調(diào)函數(shù)剃斧,發(fā)布訂閱模式,Promise三類忽你,而在ES6中提出了生成器(Generator)方式實現(xiàn),關(guān)于回調(diào)函數(shù)和發(fā)布訂閱模式實現(xiàn)可參見另一篇文章臂容,后續(xù)將推出一篇詳細(xì)介紹Promise和Generator科雳。