JavaScript異步編程

還記得一年前寫過一篇關(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:

  1. i在此處是for循環(huán)所在上下文環(huán)境的變量,有且只有一個i;
  2. 循環(huán)結(jié)束時i==5;
  3. 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)流程分解如下:

  1. 宿主環(huán)境為JavaScript創(chuàng)建線程時,會創(chuàng)建堆(heap)和棧(stack)法瑟,堆內(nèi)存儲JavaScript對象冀膝,棧內(nèi)存儲執(zhí)行上下文;
  2. 棧內(nèi)執(zhí)行上下文的同步任務(wù)按序執(zhí)行霎挟,執(zhí)行完即退棧窝剖,而當(dāng)異步任務(wù)執(zhí)行時,該異步任務(wù)進(jìn)入等待狀態(tài)(不入棧)酥夭,同時通知線程:當(dāng)觸發(fā)該事件時(或該異步操作響應(yīng)返回時)赐纱,需向消息隊列插入一個事件消息;
  3. 當(dāng)事件觸發(fā)或響應(yīng)返回時采郎,線程向消息隊列插入該事件消息(包含事件及回調(diào))千所;
  4. 當(dāng)棧內(nèi)同步任務(wù)執(zhí)行完畢后,線程從消息隊列取出一個事件消息蒜埋,其對應(yīng)異步任務(wù)(函數(shù))入棧淫痰,執(zhí)行回調(diào)函數(shù),如果未綁定回調(diào)整份,這個消息會被丟棄待错,執(zhí)行完任務(wù)后退棧;
  5. 當(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科雳。

作者:熊建剛
轉(zhuǎn)載:極樂科技專欄
轉(zhuǎn)載請標(biāo)明出處

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市脓杉,隨后出現(xiàn)的幾起案子糟秘,更是在濱河造成了極大的恐慌,老刑警劉巖球散,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尿赚,死亡現(xiàn)場離奇詭異,居然都是意外死亡蕉堰,警方通過查閱死者的電腦和手機凌净,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屋讶,“玉大人冰寻,你說我怎么就攤上這事∶笊” “怎么了斩芭?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長乐疆。 經(jīng)常有香客問我划乖,道長,這世上最難降的妖魔是什么挤土? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任琴庵,我火速辦了婚禮,結(jié)果婚禮上仰美,老公的妹妹穿的比我還像新娘细卧。我一直安慰自己,他們只是感情好筒占,可當(dāng)我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布贪庙。 她就那樣靜靜地躺著,像睡著了一般翰苫。 火紅的嫁衣襯著肌膚如雪止邮。 梳的紋絲不亂的頭發(fā)上这橙,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天,我揣著相機與錄音导披,去河邊找鬼屈扎。 笑死,一個胖子當(dāng)著我的面吹牛撩匕,可吹牛的內(nèi)容都是我干的鹰晨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼止毕,長吁一口氣:“原來是場噩夢啊……” “哼模蜡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起扁凛,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤忍疾,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谨朝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卤妒,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年字币,在試婚紗的時候發(fā)現(xiàn)自己被綠了则披。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡洗出,死狀恐怖收叶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情共苛,我是刑警寧澤判没,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站隅茎,受9級特大地震影響澄峰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辟犀,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一俏竞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧堂竟,春花似錦魂毁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至税稼,卻和暖如春烦秩,著一層夾襖步出監(jiān)牢的瞬間垮斯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工只祠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留兜蠕,地道東北人。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓抛寝,卻偏偏與公主長得像熊杨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子盗舰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內(nèi)容