詳細解析JavaScript中的異步機制

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

1、同步與異步

在介紹JavaScript的異步機制以前,首先介紹一下:什麼是同步?什麼是異步?瀏覽器

同步

若是在函數返回的時候,調用者就可以獲得預期結果(即拿到了預期的返回值或者看到了預期的效果),那麼這個函數就是同步的。 以下所示:bash

//在函數返回時,得到了預期值,即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代碼的線程執行回調。ide

若是函數是異步的,發出調用以後,立刻返回,可是不會立刻返回預期結果。調用者沒必要主動等待,當被調用者獲得結果以後會經過回調函數主動通知調用者。函數

2、單線程與多線程

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

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

瀏覽器

目前最爲流行的瀏覽器爲: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...)等都是由瀏覽器提供的其餘線程來完成的。

3、消息隊列與事件循環

經過以上了解,能夠知道其實JavaScript也是經過JS引擎線程與瀏覽器中其餘線程交互協做實現異步。可是回調函數具體什麼時候加入到JS引擎線程中執行?執行順序是怎麼樣的?

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

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

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

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

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

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

4、示例

引用一篇文章中提到的考察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
複製代碼
相關文章
相關標籤/搜索