JavaScript異步機(jī)制詳解

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

一、同步與異步

在介紹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í)就是一門(mén)語(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)常被人詬病的瀏覽器樣式兼容性問(wèn)題。這里我們不做具體討論。

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

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

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

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

所以,雖然JavaScript是單線程的,可是瀏覽器內(nèi)部不是單線程的。一些I/O操作、定時(shí)器的計(jì)時(shí)和事件監(jiān)聽(tīng)(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)。

右邊的堆用來(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ù)壓入棧中,單線程開(kāi)始執(zhí)行新的同步任務(wù)。

JS引擎線程從消息隊(duì)列中讀取任務(wù)是不斷循環(huán)的,每次棧被清空后,都會(huì)在消息隊(duì)列中讀取新的任務(wù),如果沒(méi)有新的任務(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ù)并沒(méi)有立刻執(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容