JavaScript異步機制詳解

學習JavaScript的時候了解到JavaScript是單線程的,剛開始很疑惑,單線程怎么處理網絡請求、文件讀寫等耗時操作呢?效率豈不是會很低?隨著對這方面內容的了解和深入,知道了其中的奧秘。本篇文章就主要講解一下JavaScript怎么處理異步問題。

一、同步與異步

在介紹JavaScript的異步機制之前,首先介紹一下:什么是同步?什么是異步?


同步

如果在函數返回的時候,調用者就能夠得到預期結果(即拿到了預期的返回值或者看到了預期的效果),那么這個函數就是同步的。
如下所示:

//在函數返回時,獲得了預期值,即2的平方根
Math.sqrt(2);
//在函數返回時,獲得了預期的效果,即在控制臺上打印了'hello'
console.log('hello');

上面兩個函數就是同步的。

如果函數是同步的,即使調用函數執行的任務比較耗時,也會一直等待直到得到預期結果。

異步

如果在函數返回的時候,調用者還不能夠得到預期結果,而是需要在將來通過一定的手段得到,那么這個函數就是異步的。
如下所示:

//讀取文件
fs.readFile('hello.txt', 'utf8', function(err, data) {
    console.log(data);
});
//網絡請求
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回調函數
xhr.open('GET', url);
xhr.send(); // 發起函數

上述示例中讀取文件函數 readFile和網絡請求的發起函數 send都將執行耗時操作,雖然函數會立即返回,但是不能立刻獲取預期的結果,因為耗時操作交給其他線程執行,暫時獲取不到預期結果(后面介紹)。而在JavaScript中通過回調函數 function(err, data) { console.log(data); }onreadystatechange ,在耗時操作執行完成后把相應的結果信息傳遞給回調函數,通知執行JavaScript代碼的線程執行回調。

如果函數是異步的,發出調用之后,馬上返回,但是不會馬上返回預期結果。調用者不必主動等待,當被調用者得到結果之后會通過回調函數主動通知調用者。

二、單線程與多線程


在上面介紹異步的過程中就可能會納悶:既然JavaScript是單線程,怎么還存在異步,那些耗時操作到底交給誰去執行了?

JavaScript其實就是一門語言,說是單線程還是多線程得結合具體運行環境。JS的運行通常是在瀏覽器中進行的,具體由JS引擎去解析和運行。下面我們來具體了解一下瀏覽器。

瀏覽器

目前最為流行的瀏覽器為:Chrome,IE,Safari,FireFox,Opera。瀏覽器的內核是多線程的。

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

  • 渲染引擎線程:顧名思義,該線程負責頁面的渲染
  • JS引擎線程:負責JS的解析和執行
  • 定時觸發器線程:處理定時事件,比如setTimeout, setInterval
  • 事件觸發線程:處理DOM事件
  • 異步http請求線程:處理http請求

需要注意的是,渲染線程和JS引擎線程是不能同時進行的。渲染線程在執行任務的時候,JS引擎線程會被掛起。因為JS可以操作DOM,若在渲染中JS處理了DOM,瀏覽器可能就不知所措了。

JS引擎

通常講到瀏覽器的時候,我們會說到兩個引擎:渲染引擎和JS引擎。渲染引擎就是如何渲染頁面,Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trident引擎,FireFox用的是Gecko引擎。不同的引擎對同一個樣式的實現不一致,就導致了經常被人詬病的瀏覽器樣式兼容性問題。這里我們不做具體討論。

JS引擎可以說是JS虛擬機,負責JS代碼的解析和執行。通常包括以下幾個步驟:

  • 詞法分析:將源代碼分解為有意義的分詞
  • 語法分析:用語法分析器將分詞解析成語法樹
  • 代碼生成:生成機器能運行的代碼
  • 代碼執行

不同瀏覽器的JS引擎也各不相同,Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。

之所以說JavaScript是單線程,就是因為瀏覽器在運行時只開啟了一個JS引擎線程來解析和執行JS。那為什么只有一個引擎呢?如果同時有兩個線程去操作DOM,瀏覽器是不是又要不知所措了。

所以,雖然JavaScript是單線程的,可是瀏覽器內部不是單線程的。一些I/O操作、定時器的計時和事件監聽(click, keydown...)等都是由瀏覽器提供的其他線程來完成的。

三、消息隊列與事件循環

通過以上了解,可以知道其實JavaScript也是通過JS引擎線程與瀏覽器中其他線程交互協作實現異步。但是回調函數具體何時加入到JS引擎線程中執行?執行順序是怎么樣的?

這一切的解釋就需要繼續了解消息隊列和事件循環。



如上圖所示,左邊的棧存儲的是同步任務,就是那些能立即執行、不耗時的任務,如變量和函數的初始化、事件的綁定等等那些不需要回調函數的操作都可歸為這一類。

右邊的堆用來存儲聲明的變量、對象。下面的隊列就是消息隊列,一旦某個異步任務有了響應就會被推入隊列中。如用戶的點擊事件、瀏覽器收到服務的響應和setTimeout中待執行的事件,每個異步任務都和回調函數相關聯。

JS引擎線程用來執行棧中的同步任務,當所有同步任務執行完畢后,棧被清空,然后讀取消息隊列中的一個待處理任務,并把相關回調函數壓入棧中,單線程開始執行新的同步任務。

JS引擎線程從消息隊列中讀取任務是不斷循環的,每次棧被清空后,都會在消息隊列中讀取新的任務,如果沒有新的任務,就會等待,直到有新的任務,這就叫事件循環。



上圖以AJAX異步請求為例,發起異步任務后,由AJAX線程執行耗時的異步操作,而JS引擎線程繼續執行堆中的其他同步任務,直到堆中的所有異步任務執行完畢。然后,從消息隊列中依次按照順序取出消息作為一個同步任務在JS引擎線程中執行,那么AJAX的回調函數就會在某一時刻被調用執行。

四、示例

引用一篇文章中提到的考察JavaScript異步機制的面試題來具體介紹。

執行下面這段代碼,執行后,在 5s 內點擊兩下,過一段時間(>5s)后,再點擊兩下,整個過程的輸出結果是什么?

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();

要想了解上述代碼的輸出結果,首先介紹下定時器。

setTimeout的作用是在間隔一定的時間后,將回調函數插入消息隊列中,等棧中的同步任務都執行完畢后,再執行。因為棧中的同步任務也會耗時,所以間隔的時間一般會大于等于指定的時間

setTimeout(fn, 0)的意思是,將回調函數fn立刻插入消息隊列,等待執行,而不是立即執行。看一個例子:

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

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

打印結果表明回調函數并沒有立刻執行,而是等待棧中的任務執行完畢后才執行的。棧中的任務執行多久,它就得等多久。

理解了定時器的作用,那么對于輸出結果就容易得出了。

首先,先執行同步任務。其中waitFiveSeconds是耗時操作,持續執行長達5s。

0
1
2
3
4
click begin
finished waiting

然后,在JS引擎線程執行的時候,'timer a'對應的定時器產生的回調、 'timer b'對應的定時器產生的回調和兩次 click 對應的回調被先后放入消息隊列。由于JS引擎線程空閑后,會先查看是否有事件可執行,接著再處理其他異步任務。因此會產生 下面的輸出順序。

click
click
timer a
timer b

最后,5s 后的兩次 click 事件被放入消息隊列,由于此時JS引擎線程空閑,便被立即執行了。

click
click

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容