JavaScript:完全理解同步、異步和事件循環(Event Loop)

一. 單線程

咱們常說「JavaScript是單線程的」。javascript

所謂單線程,是指在JS引擎中負責解釋和執行JavaScript代碼的線程只有一個。不妨叫它主線程java

可是實際上還存在其餘的線程。例如:處理AJAX請求的線程、處理DOM事件的線程、定時器線程、讀寫文件的線程(例如在Node.js中)等等。這些線程可能存在於JS引擎以內,也可能存在於JS引擎以外,在此咱們不作區分。不妨叫它們工做線程ajax

二. 同步和異步

假設存在一個函數A:編程

A(args...);

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

例如:異步

Math.sqrt(2); console.log('Hi');
  • 第一個函數返回時,就拿到了預期的返回值:2的平方根。函數

  • 第二個函數返回時,就看到了預期的效果:在控制檯打印了一個字符串。oop

因此這兩個函數都是同步的。post

異步:若是在函數A返回的時候,調用者還不可以獲得預期結果,而是須要在未來經過必定的手段獲得,那麼這個函數就是異步的。ui

例如:

fs.readFile('foo.txt', 'utf8', function(err, data) { console.log(data); });

在上面的代碼中,咱們但願經過fs.readFile函數讀取文件foo.txt中的內容,並打印出來。
可是在fs.readFile函數返回時,咱們指望的結果並不會發生,而是要等到文件所有讀取完成以後。若是文件很大的話可能要很長時間。

下面以AJAX請求爲例,來看一下同步和異步的區別:

  • 異步AJAX:

  • 主線程:「你好,AJAX線程。請你幫我發個HTTP請求吧,我把請求地址和參數都給你了。」

  • AJAX線程:「好的,主線程。我立刻去發,但可能要花點兒時間呢,你能夠先去忙別的。」

  • 主線程::「謝謝,你拿到響應後告訴我一聲啊。」

  • (接着,主線程作其餘事情去了。一頓飯的時間後,它收到了響應到達的通知。)

  • 同步AJAX:

  • 主線程:「你好,AJAX線程。請你幫我發個HTTP請求吧,我把請求地址和參數都給你了。」

  • AJAX線程:「......」

  • 主線程::「喂,AJAX線程,你怎麼不說話?」

  • AJAX線程:「......」

  • 主線程::「喂!喂喂喂!」

  • AJAX線程:「......」

  • (一炷香的時間後)

  • 主線程::「喂!求你說句話吧!」

  • AJAX線程:「主線程,很差意思,我在工做的時候不能說話。你的請求已經發完了,拿到響應數據了,給你。」

正是因爲JavaScript是單線程的,而異步容易實現非阻塞,因此在JavaScript中對於耗時的操做或者時間不肯定的操做,使用異步就成了必然的選擇。異步是這篇文章關注的重點。

三. 異步過程的構成要素

從上文能夠看出,異步函數實際上很快就調用完成了。可是後面還有工做線程執行異步任務、通知主線程、主線程調用回調函數等不少步驟。咱們把整個過程叫作異步過程。異步函數的調用在整個異步過程當中,只是一小部分。

總結一下,一個異步過程一般是這樣的:

主線程發起一個異步請求,相應的工做線程接收請求並告知主線程已收到(異步函數返回);主線程能夠繼續執行後面的代碼,同時工做線程執行異步任務;工做線程完成工做後,通知主線程;主線程收到通知後,執行必定的動做(調用回調函數)。

異步函數一般具備如下的形式:

A(args..., callbackFn)

它能夠叫作異步過程的發起函數,或者叫作異步任務註冊函數。args是這個函數須要的參數。callbackFn也是這個函數的參數,可是它比較特殊因此單獨列出來。

因此,從主線程的角度看,一個異步過程包括下面兩個要素:

  • 發起函數(或叫註冊函數)A

  • 回調函數callbackFn

它們都是在主線程上調用的,其中註冊函數用來發起異步過程,回調函數用來處理結果。

舉個具體的例子:

setTimeout(fn, 1000);

其中的setTimeout就是異步過程的發起函數,fn是回調函數。

注意:前面說的形式A(args..., callbackFn)只是一種抽象的表示,並不表明回調函數必定要做爲發起函數的參數,例如:

var xhr = new XMLHttpRequest(); xhr.onreadystatechange = xxx; // 添加回調函數 xhr.open('GET', url); xhr.send(); // 發起函數

發起函數和回調函數就是分離的。

四. 消息隊列和事件循環

上文講到,異步過程當中,工做線程在異步操做完成後須要通知主線程。那麼這個通知機制是怎樣實現的呢?答案是利用消息隊列和事件循環。

用一句話歸納:

工做線程將消息放到消息隊列,主線程經過事件循環過程去取消息。

  • 消息隊列:消息隊列是一個先進先出的隊列,它裏面存放着各類消息。

  • 事件循環:事件循環是指主線程重複從消息隊列中取消息、執行的過程。

實際上,主線程只會作一件事情,就是從消息隊列裏面取消息、執行消息,再取消息、再執行。當消息隊列爲空時,就會等待直到消息隊列變成非空。並且主線程只有在將當前的消息執行完成後,纔會去取下一個消息。這種機制就叫作事件循環機制,取一個消息並執行的過程叫作一次循環。

事件循環用代碼表示大概是這樣的:

while(true) { var message = queue.get(); execute(message); }

那麼,消息隊列中放的消息具體是什麼東西?消息的具體結構固然跟具體的實現有關,可是爲了簡單起見,咱們能夠認爲:

消息就是註冊異步任務時添加的回調函數。

再次以異步AJAX爲例,假設存在以下的代碼:

$.ajax('http://segmentfault.com', function(resp) { console.log('我是響應:', resp); }); // 其餘代碼 ... ... ...

主線程在發起AJAX請求後,會繼續執行其餘代碼。AJAX線程負責請求segmentfault.com,拿到響應後,它會把響應封裝成一個JavaScript對象,而後構造一條消息:

// 消息隊列中的消息就長這個樣子 var message = function () { callbackFn(response); }

其中的callbackFn就是前面代碼中獲得成功響應時的回調函數。

主線程在執行完當前循環中的全部代碼後,就會到消息隊列取出這條消息(也就是message函數),並執行它。到此爲止,就完成了工做線程對主線程的通知,回調函數也就獲得了執行。若是一開始主線程就沒有提供回調函數,AJAX線程在收到HTTP響應後,也就不必通知主線程,從而也不必往消息隊列放消息。

用圖表示這個過程就是:

從上文中咱們也能夠獲得這樣一個明顯的結論,就是:

異步過程的回調函數,必定不在當前這一輪事件循環中執行。

五. 異步與事件

上文中說的「事件循環」,爲何裏面有個事件呢?那是由於:

消息隊列中的每條消息實際上都對應着一個事件。

上文中一直沒有提到一類很重要的異步過程:DOM事件

舉例來講:

var button = document.getElement('#btn'); button.addEventListener('click', function(e) { console.log(); });

從事件的角度來看,上述代碼表示:在按鈕上添加了一個鼠標單擊事件的事件監聽器;當用戶點擊按鈕時,鼠標單擊事件觸發,事件監聽器函數被調用。

從異步過程的角度看,addEventListener函數就是異步過程的發起函數,事件監聽器函數就是異步過程的回調函數。事件觸發時,表示異步任務完成,會將事件監聽器函數封裝成一條消息放到消息隊列中,等待主線程執行。

事件的概念實際上並非必須的,事件機制實際上就是異步過程的通知機制。我以爲它的存在是爲了編程接口對開發者更友好。

另外一方面,全部的異步過程也均可以用事件來描述。例如:setTimeout能夠當作對應一個時間到了!的事件。前文的setTimeout(fn, 1000);能夠當作:

timer.addEventListener('timeout', 1000, fn);

六. 生產者與消費者

從生產者與消費者的角度看,異步過程是這樣的:

工做線程是生產者,主線程是消費者(只有一個消費者)。工做線程執行異步任務,執行完成後把對應的回調函數封裝成一條消息放到消息隊列中;主線程不斷地從消息隊列中取消息並執行,當消息隊列空時主線程阻塞,直到消息隊列再次非空。

七. 總結一下

最後再用一個生活中的例子總結一下同步和異步:在公路上,汽車一輛接一輛,有條不紊的運行。這時,有一輛車壞掉了。假如它停在原地進行修理,那麼後面的車就會被堵住無法行駛,交通就亂套了。幸虧旁邊有應急車道,能夠把故障車輛推到應急車道修理,而正常的車流不會受到任何影響。等車修好了,再從應急車道回到正常車道便可。惟一的影響就是,應急車道用多了,原來的車輛之間的順序會有點亂。

這就是同步和異步的區別。同步能夠保證順序一致,可是容易致使阻塞;異步能夠解決阻塞問題,可是會改變順序性。改變順序性其實也沒有什麼大不了的,只不過讓程序變得稍微難理解了一些 :)

PS:ECMAScript 262規範中,並無對異步、事件隊列等概念及其實現的描述。這些都是具體的JavaScript運行時環境使用的機制。本文重點是描述異步過程的原理,爲了便於理解作了不少簡化。因此文中的某些術語的使用多是不許確的,具體細節也未必是正確的,例如消息隊列中消息的結構。請讀者注意。

相關文章
相關標籤/搜索