深刻理解javascript中的事件循環event-loop

前面的話

  本文將詳細介紹javascript中的事件循環event-loopjavascript

 

線程

  javascript是單線程的語言,也就是說,同一個時間只能作一件事。而這個單線程的特性,與它的用途有關,做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?java

  爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質數組

【排隊】瀏覽器

  單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等着網絡

var i, t = Date.now()
for (i = 0; i < 100000000; i++) {} console.log(Date.now() - t) // 238

  像上面這樣,若是排隊是由於計算量大,CPU忙不過來,倒也算了數據結構

  可是,若是是網絡請求就不合適。由於一個網絡請求的資源何時返回是不可預知的,這種狀況再排隊等待就不明智了app

 

同步和異步

  因而,任務分爲同步任務和異步任務異步

【同步】函數

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

Math.sqrt(2);
console.log('Hi');

  第一個函數返回時,就拿到了預期的返回值:2的平方根;第二個函數返回時,就看到了預期的效果:在控制檯打印了一個字符串

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

【異步】

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

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

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

  因此,fs.readFile函數是異步的

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

 

異步詳解

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

  一個異步過程一般是這樣的:主線程發起一個異步請求,異步任務接收請求並告知主線程已收到(異步函數返回);主線程能夠繼續執行後面的代碼,同時異步操做開始執行;執行完成後通知主線程;主線程收到通知後,執行必定的動做(調用回調函數)

  所以,一個異步過程包括兩個要素:註冊函數和回調函數,其中註冊函數用來發起異步過程,回調函數用來處理結果

  下面的代碼中,其中的setTimeout就是異步過程的發起函數,fn是回調函數

setTimeout(fn, 1000);

  有一個很重要的問題,如何纔算是異步操做執行完成呢?對於不一樣類型的異步任務,操做完成的標準不一樣

【異步類型】

  通常而言,異步任務有如下三種類型

  一、普通事件,如click、resize等

  二、資源加載,如load、error等

  三、定時器,包括setInterval、setTimeout等

  下面對這三種類型分別舉例說明,下面代碼中,鼠標點擊div時,就表明任務執行完成了

div.onclick = () => {
  console.log('click')
}

  下面代碼中,XHR對象的readyState值爲4,即已經接收到所有響應數據了,表明任務執行完成

xhr.onreadystatechange = function(){
  if(xhr.readyState == 4){
      if(xhr.status == 200){
          //實際操做
          result.innerHTML += xhr.responseText;
      }
  }
}

  下面代碼中,過1s後,表明任務執行完成

setTimeout(() => {
  console.log('timeout')
},1000)

  對於同步任務來講,按順序執行便可;可是,對於異步任務,各任務執行的時間長短不一樣,執行完成的時間點也不一樣,主線程如何調控異步任務呢?這就用到了消息隊列

【消息隊列】

  有些文章把消息隊列稱爲任務隊列,或者叫事件隊列,總之是和異步任務相關的隊列

  能夠肯定的是,它是隊列這種先入先出的數據結構,和排隊是相似的,哪一個異步操做完成的早,就排在前面。不論異步操做什麼時候開始執行,只要異步操做執行完成,就能夠到消息隊列中排隊

  這樣,主線程在空閒的時候,就能夠從消息隊列中獲取消息並執行

  消息隊列中放的消息具體是什麼東西?消息的具體結構固然跟具體的實現有關。可是爲了簡單起見,能夠認爲:消息就是註冊異步任務時添加的回調函數。

 

可視化描述

  人們把javascript調控同步和異步任務的機制稱爲事件循環,首先來看事件循環機制的可視化描述

dataStructure

【棧】

  函數調用造成了一個棧幀

function foo(b) {
  var a = 10; return a + b + 11; } function bar(x) { var y = 3; return foo(x * y); } console.log(bar(7));

  當調用bar時,建立了第一個幀 ,幀中包含了bar的參數和局部變量。當bar調用foo時,第二個幀就被建立,並被壓到第一個幀之上,幀中包含了foo的參數和局部變量。當foo返回時,最上層的幀就被彈出棧(剩下bar函數的調用幀 )。當bar返回的時候,棧就空了

【堆】

  對象被分配在一個堆中,即用以表示一個大部分非結構化的內存區域

【隊列】

  一個 JavaScript 運行時包含了一個待處理的消息隊列。每個消息都與一個函數相關聯。當棧擁有足夠內存時,從隊列中取出一個消息進行處理。這個處理過程包含了調用與這個消息相關聯的函數(以及於是建立了一個初始堆棧幀)。當棧再次爲空的時候,也就意味着消息處理結束

 

事件循環

  下面來詳細介紹事件循環。下圖中,主線程運行的時候,產生堆和棧,棧中的代碼調用各類外部API,異步操做執行完成後,就在消息隊列中排隊。只要棧中的代碼執行完畢,主線程就會去讀取消息隊列,依次執行那些異步任務所對應的回調函數

eventloop

  詳細步驟以下:

  一、全部同步任務都在主線程上執行,造成一個執行棧

  二、主線程以外,還存在一個"消息隊列"。只要異步操做執行完成,就到消息隊列中排隊

  三、一旦執行棧中的全部同步任務執行完畢,系統就會按次序讀取消息隊列中的異步任務,因而被讀取的異步任務結束等待狀態,進入執行棧,開始執行

  四、主線程不斷重複上面的第三步

【循環】

  從代碼執行順序的角度來看,程序最開始是按代碼順序執行代碼的,遇到同步任務,馬上執行;遇到異步任務,則只是調用異步函數發起異步請求。此時,異步任務開始執行異步操做,執行完成後到消息隊列中排隊。程序按照代碼順序執行完畢後,查詢消息隊列中是否有等待的消息。若是有,則按照次序從消息隊列中把消息放到執行棧中執行。執行完畢後,再從消息隊列中獲取消息,再執行,不斷重複。

  因爲主線程不斷的重複得到消息、執行消息、再取消息、再執行。因此,這種機制被稱爲事件循環

  用代碼表示大概是這樣:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

  若是當前沒有任何消息queue.waitForMessage 會等待同步消息到達

【事件】

  爲何叫事件循環?而不叫任務循環或消息循環。究其緣由是消息隊列中的每條消息實際上都對應着一個事件

  DOM操做對應的是DOM事件,資源加載操做對應的是加載事件,而定時器操做能夠看作對應一個「時間到了」的事件

 

實例

  下面以一個實例來解釋事件循環機制

console.log(1)
div.onclick = () => {console.log('click')}
console.log(2)
setTimeout(() => {console.log('timeout')},1000)

  一、執行第一行代碼,第一行是一個同步任務,控制檯顯示1

  二、執行第二行代碼,第二行是一個異步任務,發起異步請求,能夠在任意時刻執行鼠標點擊的異步操做

  三、執行第三行代碼,第三行是一個同步任務,控制檯顯示2

  四、執行第四行代碼,第四行是一個異步任務,發起異步請求,1s後執行定時器任務

  五、假設從執行第四行代碼的1s內,執行了鼠標點擊,則鼠標任務在消息隊列中排到首位

  六、從執行第四行代碼1s後,定時器任務到消息隊列中排到第二位

  七、如今同步任務已經執行完畢,則從消息隊列中按照次序把異步任務放到執行棧中執行

  八、則控制檯依次顯示'click‘、'timeout'

  九、過了一段時間後,又執行了一次鼠標點擊,因爲消息隊列中已經空了,則鼠標任務在消息隊列中排到首位

  十、同步任務執行完畢後,再從消息隊列中按照次序把異步任務放到執行棧中執行

  十一、 則控制檯顯示'click'

【異步過程】

  下面以一個實例來解釋一次完整的異步過程

div.onclick = function fn(){console.log('click')}

  一、主線程經過調用異步函數div.onclick發起異步請求

  二、在某一時刻,執行異步操做,即鼠標點擊

  三、接着,回調函數fn到消息隊列中排隊

  四、主線程從消息隊列中讀取fn到執行棧中

  五、而後在執行棧中執行fn裏面的代碼console.log('click')

  六、因而,控制檯顯示'click'

 

同步變異步

  每個消息完整的執行後,其它消息纔會被執行。這點提供了一些優秀的特性,包括每當一個函數運行時,它就不能被搶佔,而且在其餘代碼運行以前徹底運行

  這個模型的一個缺點在於當一個消息須要太長時間才能完成,Web應用沒法處理用戶的交互,例如點擊或滾動

  因而,對於這種狀況的常見優化是同步變異步

  一個例子是建立WebQQ的QQ好友列表。列表中一般會有成百上千個好友,若是一個好友用一個節點來表示,在頁面中渲染這個列表的時候,可能要一次性往頁面中建立成百上千個節點

  在短期內往頁面中大量添加DOM節點顯然也會讓瀏覽器吃不消,看到的結果每每就是瀏覽器的卡頓甚至假死。代碼以下:

var ary = [];
for ( var i = 1; i <= 1000; i++ ){
  ary.push( i );    // 假設 ary 裝載了 1000 個好友的數據
};
var renderFriendList = function( data ){
  for ( var i = 0, l = data.length; i < l; i++ ){
    var div = document.createElement( 'div' );
    div.innerHTML = i;
    document.body.appendChild( div );
  }
};
renderFriendList( ary );

  這個問題的解決方案之一是數組分塊技術,下面的timeChunk函數讓建立節點的工做分批進行,好比把1秒鐘建立1000個節點,改成每隔200毫秒建立8個節點

function chunk(array,process,context){
    setTimeout(function(){
        //取出下一個條目並處理
        var item = array.shift();
        process.call(context,item);
        //若還有條目,再設置另外一個定時器
        if(array.length > 0){
            setTimeout(arguments.callee,100);
        }
    },100);    
}
var data = [1,2,3,4,5,6,7,8,9,0];
function printValue(item){
    var div = document.getElementById('myDiv');
    div.innerHTML += item + '<br>';
}
chunk(data.concat(),printValue);

  數組分塊的重要性在於它能夠將多個項目的處理在消息隊列上分開,在每一個項目處理以後,給予其餘的異步任務的執行機會,這樣就可能避免長時間運行腳本的錯誤。一旦某個函數須要花50ms以上的時間完成,那麼最好看看可否將任務分割爲一系列可使用定時器的小任務

相關文章
相關標籤/搜索