js異步從入門到放棄(一)- Event Loop模型

前言

異步一直是前端開發裏最讓人頭疼的一個難點,接下來的幾篇文章,將圍繞這個話題展開。html

1. 單線程的語言-JavaScript

衆所周知,JS最初的目的是用於處理瀏覽器的用戶交互和操做DOM,所以,若是JS設計成容許同時存在2個以上的線程,就會出現如下這種問題:前端

2個線程同時操做了同一個DOM節點(a線程要編輯該節點,而b線程刪除該節點),那麼此時瀏覽器將沒法處理,由於沒法判斷以哪一個線程爲基準。所以,JS只能是單線程。Web Worker API雖然提供了多線程,但只是純粹基於使用多核cpu的計算能力,其建立的子線程嚴格受控,不影響JS單線程的設計實質
,單線程的設計就意味着,任務以排隊的方式依此執行。ajax

基於單線程設計,不可避免的遇到一個情形:某些任務須要的時間很長,但不是由於任務自己太過複雜,難以處理,而是輸入輸出太慢(例如Ajax獲取數據)。而在等待輸入輸出的過程當中,CPU是閒置的,爲了充分利用資源,這一類任務被設計成容許暫時掛起,等到有告終果再執行的任務。segmentfault

如今有兩種任務了:同步任務和異步任務api

接下來介紹JS的處理機制。promise

2. Event Loop

理論基礎

首先看來自MDN的一張圖:瀏覽器

image

  • 棧(stack),函數調用堆棧。

    看這個例子:多線程

    function a(){
            console.log('a')
        }
    
        function b(){
            console.log('from')
            a() // 這裏調用了函數a
        }
        b()

    在Chrome中運行,而且單步調試,能夠看到如下步驟:
    圖1
    圖2閉包

    1. 執行b()時,函數b進棧(如圖1)
    2. b中調用函數a時,a繼續進棧(如圖2)
    3. 函數a執行完畢,出棧(如圖1)

這部份內容實際上對應着以前介紹閉包時,函數做用域鏈的生成部分,傳送門dom

  • 堆(heap),內存區,用於存儲對象。(這個目前不是很重要先不用管)
  • 隊列(queue),待處理消息隊列, 每個消息都關聯着一個用以處理這個消息的函數。

    常見示例:

    1. 讓頁面中的某個按鈕,點擊時觸發handleClick函數,那麼,當用戶觸發點擊按鈕的動做時,會有一個待處理消息進入queue,關聯的函數爲handleClick
    2. 發起一個ajax請求,當請求有結果以後,會有一個待處理消息進入queue,關聯的函數爲所指定的回調函數

總體運行過程

總體的執行過程以下(如圖):
圖片描述

  1. 主線程執行同步代碼,執行過程會產生對應的函數調用棧stack,若是碰到有異步事件,如發起ajax請求,則提交給對應的異步模塊處理,當異步任務有結果時,異步模塊負責在消息隊列中添加待處理的消息;
  2. 當同步任務處理完成,函數調用棧清空時,主線程檢查消息隊列queue:若是消息隊列不爲空,那麼從消息隊列頭部取出一個待處理的消息,進入主線程;
  3. 主線程重複以上過程

上述過程循環執行,因此稱爲事件循環(Event Loop)

// 簡單的例子
 var req = new XMLHttpRequest(); 
    req.open('GET', url);    
    req.onload = function (){}; //指定回調函數, 這是一個異步任務,會被先提交到異步處理的api,等有告終果纔會添加到消息隊列
    req.send();

*任務隊列類型

補充說明如下,任務隊列分紅2類:

  1. microtask queue:ES6 的 promise產生的任務隊列
  2. macrotask queue:除microtask queue之外的任務產生的任務隊列,如(事件觸發 setTimeout Ajax請求)

他們的區別下次講解Promise時再說明(挖個坑)

3.定時器

上述Event Loop模型中,消息隊列的新消息來源,除了有dom事件操做,ajax請求等,也多是定時任務,也就是由setTimeout建立的任務。這個函數你們確定不陌生,可是也可能未必真的足夠熟悉~。

setTimeout接受兩個參數:

  1. 回調函數
  2. 延遲執行的毫秒數。(嚴格來講,應該是實際加入到主線程的最小延遲時間,爲何呢,往下看)

如今看下如下2個例子:

//示例1
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);
// 輸出結果 1 3 2 ,由於setTimeout指定了裏面的函數要推遲1000毫秒纔會執行

這個例子說明了setTimeout的基本做用,比較簡單很少說。

//示例2
const s = new Date().getSeconds(); //獲取當前的秒數
setTimeout(function() {
  // 輸出 "2",表示回調函數並無在 500 毫秒以後當即執行
  console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);

while(true) {//這個循環含義就是,至少要過2s,當前主線程任務才執行完畢
  if(new Date().getSeconds() - s >= 2) { 
    console.log("Good, looped for 2 seconds");
    break;
  }
}

//實際輸出
Good, looped for 2 seconds
eventloop.html:15 Ran after 2 seconds

這個例子,首先使用setTimeout指定了一個500毫秒後執行的回調函數,而後使用while循環故意讓當前運行超過2秒鐘,根據上文的流程圖可知:

其實在第500毫秒時,這個消息已經被添加到消息隊列,可是因爲當前的主線程並無執行完,調用棧還沒有清空,因此在500毫秒不會執行setTimeout指定的回調函數。實際上,即便把上述代碼中的500改爲0,結果也是同樣的。

簡而言之,setTimeout(fn,x毫秒)的x只是指定了fn被執行的最小等待時間,息具體能在多少時間以後執行,取決於現有調用棧函數的執行進度,以及消息隊列中前面的任務執行進度。

小結

本文介紹了Event Loop模型過程以及常見的任務隊列的幾種任務隊列消息來源,這是JS異步話題的基礎篇。

參考文獻
MDN-EventLoop
JavaScript 運行機制詳解:再談Event Loop


慣例:若是內容有錯誤的地方歡迎指出(以爲看着不理解不舒服想吐槽也徹底沒問題);若是有幫助,歡迎點贊和收藏,轉載請徵得贊成後著明出處,若是有問題也歡迎私信交流,主頁有郵箱地址

相關文章
相關標籤/搜索