讓你高效的理解JavaScript中的同步、異步和事件循環


  “同步請求”,“異步請求”相信這兩詞在程序猿的世界中頻頻出現,到底是詞性的妖嬈,還是擼代碼的基礎要求,下面直接分享本人學習的好東西,保證讓你深入淺出,爽得不要不要的。

  一、單線程

  我們常說的“JavaScript是單線程的”。所謂單線程,是指在JS引擎中負責解釋和執行JavaScript代碼的線程只有一個,一般稱它為主線程。但是實際上還存在其他的線程,例如:處理AJAX請求的線程、處理DOM事件的線程、定時器線程、讀寫文件的線程等等。這些線程可能存在於JS引擎之內,也可能存在於JS引擎之外,在此我們不做區分,不妨稱它們為工作線程吧。

  

  二、同步和異步

  假設存在一個函數A:

  A(args...){

    ...

  };

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

  例:1、 Math.sqrt(2); 

    2、console.log('這是我們經常用的好東西');

    第一個函數返回時,就拿到了我們預期的返回值,2的平方;第二個函數返回時,就能看到預期的結果,我們在控制台打印了一個字符串,所以這兩個函數都是同步的

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

  例:fs.readFile( 'foo.txt', 'utf8', function(err, data) {

      console.log(data);

    });

  在上面的栗子中,我們希望通過fs.readFile函數讀取文件foo.txt中的內容,並打印出來。但是在fs.readFile函數返回值之前,我們期望的結果並不會發生,而是要等到文件全部讀取完成之后(如果文件很大的話,可能需要的時間會長一點),這就是異步。

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

  異步AJAX:

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

    AJAX線程:“好的,主線程。我馬上去發,但是可能需要花點時間呢,你可以先去忙別的。”

    主線程:“謝謝,你拿到響應之后告訴我一聲哈。”

    (注:主線程和AJAX線程都各自同步干活,一段時間后,主線程就能收到AJAX線程響應的通知,然后繼續執行相應的工作)。

  同步AJAX:

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

    AJAX線程:“......”

    主線程:“喂,AJAX線程,你怎么不說話?”

    AJAX線程:“......” 

    主線程:“喂!喂喂喂!”

    AJAX線程:“......”

    (一炷香的時間后)

    主線程:“喂!求你說句話吧!”

    AJAX線程:“主線程,不好意思,我在工作的時候不能說話,你的請求已經發完了,拿到響應的數據了,給你。”

    (注:同步AJAX的主線程和AJAX線程不能同步干活,只能等AJAX線程干完活拿到響應后,只線程才能接着干活

  正是由於JavaScript是單線程,而異步容易實現非阻塞,所以在JavaScript中對於耗時的操作或者時間不確定的操作,實用異步就成了必然的選擇。

 

  三、異步過程的構成要素

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

  總結一下,一個異步過程的整個過程:主線程發一起一個異步請求,相應的工作線程接收請求並告知主線程已收到通知(異步函數返回);主線程可以繼續執行后面的代碼,同時工作線程執行異步任務;工作線程完成工作后,通知主線程;主線程收到通知后,執行一定的動作(調用回調函數)。

  異步函數通常具有以下的形式:A(args..., callbackFn);

  它可以叫做異步過程的發起函數,或者叫做異步任務注冊函數。args是這個函數需要的參數,callbackFn(回調函數)也是這個函數的參數,但是它比較特殊所以單獨列出來。所以,從主線程的角度看,一個異步過程包括下面兩個要素:

  1、發起函數(或叫注冊函數)A;

  2、回調函數callbackFn;

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

  舉個具體的栗子:

  setTimeout(function,1000);

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

  注:前面說得形式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://baidu.com',function(resp){

    console.log('我是響應',resp);

  })

  // 其他代碼

  ...

  ...

  主線程在發起AJAX請求后,會繼續執行其他代碼,AJAX線程負責請求http://baidu.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);

 

  六. 生產者與消費者

 

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

 

  

  七. 總結一下

 

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

  

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM