JavaScript異步機(jī)制詳解

學(xué)習(xí)JavaScript的時(shí)候了解到JavaScript是單線程的,剛開始很疑惑籍铁,單線程怎么處理網(wǎng)絡(luò)請(qǐng)求涡上、文件讀寫等耗時(shí)操作呢?效率豈不是會(huì)很低拒名?隨著對(duì)這方面內(nèi)容的了解和深入吩愧,知道了其中的奧秘。本篇文章就主要講解一下JavaScript怎么處理異步問題增显。

一雁佳、同步與異步

在介紹JavaScript的異步機(jī)制之前,首先介紹一下:什么是同步同云?什么是異步糖权?


同步

如果在函數(shù)返回的時(shí)候,調(diào)用者就能夠得到預(yù)期結(jié)果(即拿到了預(yù)期的返回值或者看到了預(yù)期的效果)炸站,那么這個(gè)函數(shù)就是同步的星澳。
如下所示:

//在函數(shù)返回時(shí),獲得了預(yù)期值旱易,即2的平方根
Math.sqrt(2);
//在函數(shù)返回時(shí)禁偎,獲得了預(yù)期的效果腿堤,即在控制臺(tái)上打印了'hello'
console.log('hello');

上面兩個(gè)函數(shù)就是同步的。

如果函數(shù)是同步的如暖,即使調(diào)用函數(shù)執(zhí)行的任務(wù)比較耗時(shí)笆檀,也會(huì)一直等待直到得到預(yù)期結(jié)果。

異步

如果在函數(shù)返回的時(shí)候盒至,調(diào)用者還不能夠得到預(yù)期結(jié)果酗洒,而是需要在將來(lái)通過(guò)一定的手段得到,那么這個(gè)函數(shù)就是異步的枷遂。
如下所示:

//讀取文件
fs.readFile('hello.txt', 'utf8', function(err, data) {
    console.log(data);
});
//網(wǎng)絡(luò)請(qǐng)求
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回調(diào)函數(shù)
xhr.open('GET', url);
xhr.send(); // 發(fā)起函數(shù)

上述示例中讀取文件函數(shù) readFile和網(wǎng)絡(luò)請(qǐng)求的發(fā)起函數(shù) send都將執(zhí)行耗時(shí)操作樱衷,雖然函數(shù)會(huì)立即返回,但是不能立刻獲取預(yù)期的結(jié)果登淘,因?yàn)楹臅r(shí)操作交給其他線程執(zhí)行箫老,暫時(shí)獲取不到預(yù)期結(jié)果(后面介紹)封字。而在JavaScript中通過(guò)回調(diào)函數(shù) function(err, data) { console.log(data); }onreadystatechange 黔州,在耗時(shí)操作執(zhí)行完成后把相應(yīng)的結(jié)果信息傳遞給回調(diào)函數(shù),通知執(zhí)行JavaScript代碼的線程執(zhí)行回調(diào)阔籽。

如果函數(shù)是異步的流妻,發(fā)出調(diào)用之后,馬上返回笆制,但是不會(huì)馬上返回預(yù)期結(jié)果绅这。調(diào)用者不必主動(dòng)等待,當(dāng)被調(diào)用者得到結(jié)果之后會(huì)通過(guò)回調(diào)函數(shù)主動(dòng)通知調(diào)用者在辆。

二证薇、單線程與多線程


在上面介紹異步的過(guò)程中就可能會(huì)納悶:既然JavaScript是單線程,怎么還存在異步匆篓,那些耗時(shí)操作到底交給誰(shuí)去執(zhí)行了浑度?

JavaScript其實(shí)就是一門語(yǔ)言,說(shuō)是單線程還是多線程得結(jié)合具體運(yùn)行環(huán)境鸦概。JS的運(yùn)行通常是在瀏覽器中進(jìn)行的箩张,具體由JS引擎去解析和運(yùn)行。下面我們來(lái)具體了解一下瀏覽器窗市。

瀏覽器

目前最為流行的瀏覽器為:Chrome先慷,IE,Safari咨察,F(xiàn)ireFox论熙,Opera。瀏覽器的內(nèi)核是多線程的摄狱。

一個(gè)瀏覽器通常由以下幾個(gè)常駐的線程:

  • 渲染引擎線程:顧名思義脓诡,該線程負(fù)責(zé)頁(yè)面的渲染
  • JS引擎線程:負(fù)責(zé)JS的解析和執(zhí)行
  • 定時(shí)觸發(fā)器線程:處理定時(shí)事件素跺,比如setTimeout, setInterval
  • 事件觸發(fā)線程:處理DOM事件
  • 異步http請(qǐng)求線程:處理http請(qǐng)求

需要注意的是,渲染線程和JS引擎線程是不能同時(shí)進(jìn)行的誉券。渲染線程在執(zhí)行任務(wù)的時(shí)候指厌,JS引擎線程會(huì)被掛起。因?yàn)镴S可以操作DOM踊跟,若在渲染中JS處理了DOM踩验,瀏覽器可能就不知所措了。

JS引擎

通常講到瀏覽器的時(shí)候商玫,我們會(huì)說(shuō)到兩個(gè)引擎:渲染引擎和JS引擎箕憾。渲染引擎就是如何渲染頁(yè)面,Chrome/Safari/Opera用的是Webkit引擎拳昌,IE用的是Trident引擎袭异,F(xiàn)ireFox用的是Gecko引擎。不同的引擎對(duì)同一個(gè)樣式的實(shí)現(xiàn)不一致炬藤,就導(dǎo)致了經(jīng)常被人詬病的瀏覽器樣式兼容性問題御铃。這里我們不做具體討論。

JS引擎可以說(shuō)是JS虛擬機(jī)沈矿,負(fù)責(zé)JS代碼的解析和執(zhí)行上真。通常包括以下幾個(gè)步驟:

  • 詞法分析:將源代碼分解為有意義的分詞
  • 語(yǔ)法分析:用語(yǔ)法分析器將分詞解析成語(yǔ)法樹
  • 代碼生成:生成機(jī)器能運(yùn)行的代碼
  • 代碼執(zhí)行

不同瀏覽器的JS引擎也各不相同,Chrome用的是V8羹膳,F(xiàn)ireFox用的是SpiderMonkey睡互,Safari用的是JavaScriptCore,IE用的是Chakra陵像。

之所以說(shuō)JavaScript是單線程就珠,就是因?yàn)闉g覽器在運(yùn)行時(shí)只開啟了一個(gè)JS引擎線程來(lái)解析和執(zhí)行JS。那為什么只有一個(gè)引擎呢醒颖?如果同時(shí)有兩個(gè)線程去操作DOM妻怎,瀏覽器是不是又要不知所措了。

所以图贸,雖然JavaScript是單線程的蹂季,可是瀏覽器內(nèi)部不是單線程的。一些I/O操作疏日、定時(shí)器的計(jì)時(shí)和事件監(jiān)聽(click, keydown...)等都是由瀏覽器提供的其他線程來(lái)完成的偿洁。

三、消息隊(duì)列與事件循環(huán)

通過(guò)以上了解沟优,可以知道其實(shí)JavaScript也是通過(guò)JS引擎線程與瀏覽器中其他線程交互協(xié)作實(shí)現(xiàn)異步涕滋。但是回調(diào)函數(shù)具體何時(shí)加入到JS引擎線程中執(zhí)行?執(zhí)行順序是怎么樣的挠阁?

這一切的解釋就需要繼續(xù)了解消息隊(duì)列和事件循環(huán)宾肺。



如上圖所示溯饵,左邊的棧存儲(chǔ)的是同步任務(wù),就是那些能立即執(zhí)行锨用、不耗時(shí)的任務(wù)丰刊,如變量和函數(shù)的初始化、事件的綁定等等那些不需要回調(diào)函數(shù)的操作都可歸為這一類增拥。

右邊的堆用來(lái)存儲(chǔ)聲明的變量啄巧、對(duì)象。下面的隊(duì)列就是消息隊(duì)列掌栅,一旦某個(gè)異步任務(wù)有了響應(yīng)就會(huì)被推入隊(duì)列中秩仆。如用戶的點(diǎn)擊事件、瀏覽器收到服務(wù)的響應(yīng)和setTimeout中待執(zhí)行的事件猾封,每個(gè)異步任務(wù)都和回調(diào)函數(shù)相關(guān)聯(lián)澄耍。

JS引擎線程用來(lái)執(zhí)行棧中的同步任務(wù),當(dāng)所有同步任務(wù)執(zhí)行完畢后晌缘,棧被清空齐莲,然后讀取消息隊(duì)列中的一個(gè)待處理任務(wù),并把相關(guān)回調(diào)函數(shù)壓入棧中枚钓,單線程開始執(zhí)行新的同步任務(wù)铅搓。

JS引擎線程從消息隊(duì)列中讀取任務(wù)是不斷循環(huán)的,每次棧被清空后搀捷,都會(huì)在消息隊(duì)列中讀取新的任務(wù),如果沒有新的任務(wù)多望,就會(huì)等待嫩舟,直到有新的任務(wù),這就叫事件循環(huán)怀偷。



上圖以AJAX異步請(qǐng)求為例家厌,發(fā)起異步任務(wù)后,由AJAX線程執(zhí)行耗時(shí)的異步操作椎工,而JS引擎線程繼續(xù)執(zhí)行堆中的其他同步任務(wù)饭于,直到堆中的所有異步任務(wù)執(zhí)行完畢。然后维蒙,從消息隊(duì)列中依次按照順序取出消息作為一個(gè)同步任務(wù)在JS引擎線程中執(zhí)行掰吕,那么AJAX的回調(diào)函數(shù)就會(huì)在某一時(shí)刻被調(diào)用執(zhí)行。

四颅痊、示例

引用一篇文章中提到的考察JavaScript異步機(jī)制的面試題來(lái)具體介紹殖熟。

執(zhí)行下面這段代碼,執(zhí)行后斑响,在 5s 內(nèi)點(diǎn)擊兩下菱属,過(guò)一段時(shí)間(>5s)后钳榨,再點(diǎn)擊兩下,整個(gè)過(guò)程的輸出結(jié)果是什么纽门?

setTimeout(function(){
    for(var i = 0; i < 100000000; i++){}
    console.log('timer a');
}, 0)

for(var j = 0; j < 5; j++){
    console.log(j);
}

setTimeout(function(){
    console.log('timer b');
}, 0)

function waitFiveSeconds(){
    var now = (new Date()).getTime();
    while(((new Date()).getTime() - now) < 5000){}
    console.log('finished waiting');
}

document.addEventListener('click', function(){
    console.log('click');
})

console.log('click begin');
waitFiveSeconds();

要想了解上述代碼的輸出結(jié)果薛耻,首先介紹下定時(shí)器。

setTimeout的作用是在間隔一定的時(shí)間后赏陵,將回調(diào)函數(shù)插入消息隊(duì)列中昭卓,等棧中的同步任務(wù)都執(zhí)行完畢后,再執(zhí)行瘟滨。因?yàn)闂V械耐饺蝿?wù)也會(huì)耗時(shí)候醒,所以間隔的時(shí)間一般會(huì)大于等于指定的時(shí)間

setTimeout(fn, 0)的意思是杂瘸,將回調(diào)函數(shù)fn立刻插入消息隊(duì)列倒淫,等待執(zhí)行,而不是立即執(zhí)行败玉〉型粒看一個(gè)例子:

setTimeout(function() {
    console.log("a")
}, 0)

for(let i=0; i<10000; i++) {}
console.log("b")
b  a

打印結(jié)果表明回調(diào)函數(shù)并沒有立刻執(zhí)行,而是等待棧中的任務(wù)執(zhí)行完畢后才執(zhí)行的运翼。棧中的任務(wù)執(zhí)行多久返干,它就得等多久。

理解了定時(shí)器的作用血淌,那么對(duì)于輸出結(jié)果就容易得出了矩欠。

首先,先執(zhí)行同步任務(wù)悠夯。其中waitFiveSeconds是耗時(shí)操作癌淮,持續(xù)執(zhí)行長(zhǎng)達(dá)5s。

0
1
2
3
4
click begin
finished waiting

然后沦补,在JS引擎線程執(zhí)行的時(shí)候乳蓄,'timer a'對(duì)應(yīng)的定時(shí)器產(chǎn)生的回調(diào)、 'timer b'對(duì)應(yīng)的定時(shí)器產(chǎn)生的回調(diào)和兩次 click 對(duì)應(yīng)的回調(diào)被先后放入消息隊(duì)列夕膀。由于JS引擎線程空閑后虚倒,會(huì)先查看是否有事件可執(zhí)行,接著再處理其他異步任務(wù)产舞。因此會(huì)產(chǎn)生 下面的輸出順序魂奥。

click
click
timer a
timer b

最后,5s 后的兩次 click 事件被放入消息隊(duì)列庞瘸,由于此時(shí)JS引擎線程空閑捧弃,便被立即執(zhí)行了。

click
click

參考文章
JavaScript:徹底理解同步、異步和事件循環(huán)(Event Loop)
從setTimeout說(shuō)事件循環(huán)模型
JavaScript單線程和異步機(jī)制
JavaScript的單線程機(jī)制
JavaScript單線程異步的背后——事件循環(huán)機(jī)制
JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末违霞,一起剝皮案震驚了整個(gè)濱河市嘴办,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌买鸽,老刑警劉巖涧郊,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異眼五,居然都是意外死亡妆艘,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門看幼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)批旺,“玉大人,你說(shuō)我怎么就攤上這事诵姜∑螅” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵棚唆,是天一觀的道長(zhǎng)暇赤。 經(jīng)常有香客問我,道長(zhǎng)宵凌,這世上最難降的妖魔是什么鞋囊? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮瞎惫,結(jié)果婚禮上溜腐,老公的妹妹穿的比我還像新娘。我一直安慰自己微饥,他們只是感情好逗扒,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著欠橘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪现恼。 梳的紋絲不亂的頭發(fā)上肃续,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音叉袍,去河邊找鬼始锚。 笑死,一個(gè)胖子當(dāng)著我的面吹牛喳逛,可吹牛的內(nèi)容都是我干的瞧捌。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼姐呐!你這毒婦竟也來(lái)了殿怜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤曙砂,失蹤者是張志新(化名)和其女友劉穎头谜,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸠澈,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡柱告,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了笑陈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片际度。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖涵妥,靈堂內(nèi)的尸體忽然破棺而出乖菱,到底是詐尸還是另有隱情,我是刑警寧澤妹笆,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布块请,位于F島的核電站,受9級(jí)特大地震影響拳缠,放射性物質(zhì)發(fā)生泄漏墩新。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一窟坐、第九天 我趴在偏房一處隱蔽的房頂上張望海渊。 院中可真熱鬧,春花似錦哲鸳、人聲如沸臣疑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)讯沈。三九已至,卻和暖如春婿奔,著一層夾襖步出監(jiān)牢的瞬間缺狠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工萍摊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挤茄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓冰木,卻偏偏與公主長(zhǎng)得像穷劈,于是被迫代替她去往敵國(guó)和親笼恰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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