轉載:JavaScript線程機制與事件機制

引子

本文介紹JavaScript運行機制,這一部分比較抽象,咱們先從一道面試題入手:javascript

console.log(1);
setTimeout(function(){
console.log(3);
},0);
console.log(2);

請問數字打印順序是什麼?題目的答案是依次輸出1 2 3 css

再看一道題html

  <div class="test">測試內容</div>
  <script>
    $('.test').text('內容改變')  alert($('.test').text())  // 頁面加載後首先時alert彈出,alert的內容爲 ‘內容改變’,而dom此時還未發生變化,dom的內容是‘測試內容’ // 而後會一直堵塞,直到咱們點擊確認,alert消失時,頁面內容纔會變成‘內容改變’ // 因此alert prompt confirm等提示框都會跳過頁面渲染先執行 </script>

解決這個問題以前先了解一下它是怎麼致使的,而要了解它須要從 JavaScript 的線程模型提及。前端

JavaScript 引擎是單線程運行的,瀏覽器不管在何時都只且只有一個線程在運行 JavaScript 程序,初衷是爲了減小 DOM 等共享資源的衝突。但是單線程永遠會面臨着一個問題,那就是某一段代碼阻塞會致使後續全部的任務都延遲。又因爲 JavaScript 常常須要操做頁面 DOM 和發送 HTTP 請求,這些 I/O 操做耗時通常都比較長,一旦阻塞,就會給用戶很是差的使用體驗。vue

因而便有了事件循環(event loop)的產生,JavaScript 將一些異步操做或 有I/O 阻塞的操做全都放到一個事件隊列,先順序執行同步 CPU代碼,等到 JavaScript 引擎沒有同步代碼,CPU 空閒下來再讀取事件隊列的異步事件來依次執行。html5

這些事件包括:java

  • setTimeout() 設置的異步延遲事件;
  • DOM 操做相關如佈局和繪製事件;
  • 網絡 I/O 如 AJAX 請求事件;
  • 用戶操做事件,如鼠標點擊、鍵盤敲擊。

明白了原理, 再解決這個問題就有了方向,咱們來分析這個問題:node

  1. 因爲頁面渲染是 DOM 操做,會被 JavaScript 引擎放入事件隊列;
  2. alert() 是 window 的內置函數,被認爲是同步 CPU代碼;
  3. JavaScript 引擎會優先執行同步代碼,alert 彈窗先出現;
  4. alert 有特殊的阻塞性質,JavaScript 引擎的執行被阻塞住;
  5. 點擊 alert 的「肯定」,JavaScript 沒有了阻塞,執行完同步代碼後,又讀取事件隊列裏的 DOM 操做,頁面渲染完成。

由上述緣由,致使了詭異的 「Alert執行順序問題」。 咱們沒法將頁面渲染變成同步操做,那麼只好把 alert() 變爲異步代碼,從而才能在頁面渲染以後執行。git

解決方法:github

setTimeout() 使用它,能夠延遲執行某些代碼。而對於延遲執行的代碼,JavaScript 引擎老是把這些代碼放到事件隊列裏去,再去檢查是否已經到了執行時間,再適時執行。代碼進入事件隊列,就意味着代碼變成和頁面渲染事件同樣異步了。因爲事件隊列是有序的,咱們若是用 setTimeout 延時執行,就能夠實如今頁面渲染以後執行 alert 的功能了。

setTimeout 的函數原型爲 setTimeout(code, msec),code 是要變爲異步的代碼或函數,msec 是要延時的時間,單位爲毫秒。這裏咱們不須要它延時,只須要它變爲異步就好了,因此能夠將 msec 設置爲 0;

  <div class="test">測試內容</div>
  <script>
    $('.test').text('內容改變') setTimeout(function(){ alert($('.test').text()) },0)  // 因爲使用setTimeout操做,使alert被放入到事件隊列 , // 同時 $('.test').text('內容改變')也是在事件隊列 // 因此 dom和alert的內容都是 ‘內容改變’

 下面重點介紹下JavaScript線程機制與事件機制

 

 

1、進程與線程

1.進程

進程是指程序的一次執行,它佔有一片獨有的內存空間,能夠經過windows任務管理器查看進程(以下圖)。同一個時間裏,同一個計算機系統中容許兩個或兩個以上的進程處於並行狀態,這是多進程。好比電腦同時運行微信,QQ,以及各類瀏覽器等。瀏覽器運行是有些是單進程,如firefox和老版IE,有些是多進程,如chrome和新版IE。

2.線程

有些進程還不止同時幹一件事,好比Word,它能夠同時進行打字、拼寫檢查、打印等事情。在一個進程內部,要同時幹多件事,就須要同時運行多個「子任務」,咱們把進程內的這些「子任務」稱爲線程(Thread)。
線程是指CPU的基本調度單位,是程序執行的一個完整流程,是進程內的一個獨立執行單元。多線程是指在一個進程內, 同時有多個線程運行。瀏覽器運行是多線程。好比用瀏覽器一邊下載,一邊聽歌,一邊看視頻。另外咱們須要知道JavaScript語言的一大特色就是單線程爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。

因爲每一個進程至少要幹一件事,因此,一個進程至少有一個線程。固然,像Word這種複雜的進程能夠有多個線程,多個線程能夠同時執行,多線程的執行方式和多進程是同樣的,也是由操做系統在多個線程之間快速切換,讓每一個線程都短暫地交替運行,看起來就像同時執行同樣。固然,真正地同時執行多線程須要多核CPU纔可能實現。

3.進程與線程

  • 應用程序必須運行在某個進程的某個線程上

  • 一個進程中至少有一個運行的線程: 主線程, 進程啓動後自動建立

  • 一個進程中若是同時運行多個線程, 那這個程序是多線程運行的

  • 一個進程的內存空間是共享的,每一個線程均可以使用這些共享內存。

  • 多個進程之間的數據是不能直接共享的

用官方術語描述:

進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)

線程是cpu調度的最小單位(線程是創建在進程的基礎上的一次程序運行單位,一個進程中能夠有多個線程)

進程和線程能夠形象比喻爲:

進程是一個工廠,工廠有它的獨立資源-工廠之間相互獨立-線程是工廠中的工人,多個工人協做完成任務-工廠內有一個或多個工人-工人之間共享空間

線程和進程區分不清,是不少新手都會犯的錯誤,沒有關係。這很正常。先看看下面這個形象的比喻:

- 進程是一個工廠,工廠有它的獨立資源

- 工廠之間相互獨立

- 線程是工廠中的工人,多個工人協做完成任務

- 工廠內有一個或多個工人

- 工人之間共享空間

再完善完善概念:

- 工廠的資源 -> 系統分配的內存(獨立的一塊內存)

- 工廠之間的相互獨立 -> 進程之間相互獨立

- 多個工人協做完成任務 -> 多個線程在進程中協做完成任務

- 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成

- 工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)

而後再鞏固下:

若是是windows電腦中,能夠打開任務管理器,能夠看到有一個後臺進程列表。對,那裏就是查看進程的地方,並且能夠看到每一個進程的內存資源信息以及cpu佔有率。

因此,應該更容易理解了:進程是cpu資源分配的最小單位(系統會給它分配內存)

最後,再用較爲官方的術語描述一遍:

  • 進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)
  • 線程是cpu調度的最小單位(線程是創建在進程的基礎上的一次程序運行單位,一個進程中能夠有多個線程)

tips

  • 不一樣進程之間也能夠通訊,不過代價較大
  • 如今,通常通用的叫法:單線程與多線程,都是指在一個進程內的單和多。(因此核心仍是得屬於一個進程才行)

4.單線程與多線程的優缺點?

單線程的優勢:順序編程簡單易懂

單線程的缺點:效率低

多線程的優勢:能有效提高CPU的利用率

多線程的缺點:

  • 建立多線程開銷
  • 線程間切換開銷
  • 死鎖與狀態同步問題

2、瀏覽器內核

瀏覽器的內核是指支持瀏覽器運行的最核心的程序,分爲兩個部分的,一是渲染引擎,另外一個是JS引擎。如今JS引擎比較獨立,內核更加傾向於說渲染引擎。

1.不一樣的瀏覽器可能不太同樣

  • Chrome, Safari: webkit
  • firefox: Gecko
  • IE: Trident
  • 360,搜狗等國內瀏覽器: Trident + webkit

2.內核由不少模塊組成

  • html,css文檔解析模塊 : 負責頁面文本的解析
  • dom/css模塊 : 負責dom/css在內存中的相關處理
  • 佈局和渲染模塊 : 負責頁面的佈局和效果的繪製
  • 定時器模塊 : 負責定時器的管理
  • 網絡請求模塊 : 負責服務器請求(常規/Ajax)
  • 事件響應模塊 : 負責事件的管理

3.瀏覽器是多線程

雖然JS運行在瀏覽器中,是單線程的,每一個window一個JS線程,但瀏覽器不是單線程的,例如Webkit或是Gecko引擎,均可能有以下線程:

  • javascript引擎線程
  • 界面渲染線程
  • 瀏覽器事件觸發線程
  • Http請求線程

不少童鞋搞不清,若是js是單線程的,那麼誰去輪詢大的Event loop事件隊列?答案是瀏覽器會有單獨的線程去處理這個隊列。

 

重點是瀏覽器內核(渲染進程)

重點來了,咱們能夠看到,上面提到了這麼多的進程,那麼,對於普通的前端操做來講,最終要的是什麼呢?答案是渲染進程

能夠這樣理解,頁面的渲染,JS的執行,事件的循環,都在這個進程內進行。接下來重點分析這個進程

請牢記,瀏覽器的渲染進程是多線程的(這點若是不理解,請回頭看進程和線程的區分

終於到了線程這個概念了?,好親切。那麼接下來看看它都包含了哪些線程(列舉一些主要常駐線程):

  1. GUI渲染線程

    • 負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。
    • 當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時,該線程就會執行
    • 注意,GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(至關於被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行。
  2. JS引擎線程

    • 也稱爲JS內核,負責處理Javascript腳本程序。(例如V8引擎)
    • JS引擎線程負責解析Javascript腳本,運行代碼。
    • JS引擎一直等待着任務隊列中任務的到來,而後加以處理,一個Tab頁(renderer進程)中不管何時都只有一個JS線程在運行JS程序
    • 一樣注意,GUI渲染線程與JS引擎線程是互斥的,因此若是JS執行的時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞。
  3. 事件觸發線程

    • 歸屬於瀏覽器而不是JS引擎,用來控制事件循環(能夠理解,JS引擎本身都忙不過來,須要瀏覽器另開線程協助)
    • 當JS引擎執行代碼塊如setTimeOut時(也可來自瀏覽器內核的其餘線程,如鼠標點擊、AJAX異步請求等),會將對應任務添加到事件線程中
    • 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理
    • 注意,因爲JS的單線程關係,因此這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)

  4. 定時觸發器線程

    • 傳說中的setIntervalsetTimeout所在線程
    • 瀏覽器定時計數器並非由JavaScript引擎計數的,(由於JavaScript引擎是單線程的, 若是處於阻塞線程狀態就會影響記計時的準確)
    • 所以經過單獨線程來計時並觸發定時(計時完畢後,添加到事件隊列中,等待JS引擎空閒後執行)
    • 注意,W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算爲4ms。
  5. 異步http請求線程

    • 在XMLHttpRequest在鏈接後是經過瀏覽器新開一個線程請求
    • 將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中。再由JavaScript引擎執行。

 

3、定時器引起的思考

1. 定時器真是定時執行的嗎?

咱們先來看個例子,試問定時器會保證200ms後執行嗎?

document.getElementById('btn').onclick = function () {
      var start = Date.now()
      console.log('啓動定時器前...')
      setTimeout(function () {
        console.log('定時器執行了', Date.now() - start)
      }, 200)
      console.log('啓動定時器後...')
      // 作一個長時間的工做
      for (var i = 0; i < 1000000000; i++) {
      }
    }


事實上,通過了625ms後定時器才執行。定時器並不能保證真正定時執行,通常會延遲一丁點,也有可能延遲很長時間(好比上面的例子)

2.定時器回調函數是在分線程執行的嗎?

定時器回調函數在主線程執行的, 具體實現方式下文會介紹。

4、瀏覽器的事件循環(輪詢)模型

1. 爲何JavaScript是單線程

JavaScript語言的一大特色就是單線程,也就是說,同一個時間只能作一件事。即單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等着。

那麼,爲何JavaScript不能有多個線程呢?這樣能提升效率啊。

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

因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。
爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。

2.Event Loop

JavaScript語言的設計者意識到單線程意味着排隊,同一時間只能作一件事,因此將全部任務分紅兩種,一種是同步任務(synchronous),另外一種是異步任務(asynchronous)如各類瀏覽器事件、定時器和Ajax等

同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;

例如:

console.log("A"); while(true){ } console.log("B"); 請問最後的輸出結果是什麼?

若是你的回答是A,恭喜你答對了,由於這是同步任務,程序由上到下執行,遇到while()死循環,下面語句就沒辦法執行。

異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。

例如:
console.log("A"); setTimeout(function(){ console.log("B"); },0); while(true){} 請問最後的輸出結果是什麼?

若是你的答案是A,恭喜你如今對js運行機制已經有個粗淺的認識了!題目中的setTimeout()就是個異步任務。在全部同步任務執行完以前,任何的異步任務是不會執行的

具體來講,異步執行的運行機制以下。(同步執行也是如此,由於它能夠被視爲沒有異步任務的異步執行。)

(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。

(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。

(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。

(4)主線程不斷重複上面的第三步

ps:

js執行棧又可稱事件循環:(事件循環是指主線程重複從消息隊列中取消息、執行的過程)

js任務隊列又可稱消息隊列(消息隊列是一個先進先出的隊列,它裏面存放着各類消息,消息可簡單理解爲回調函數)

主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)

通常來講,有如下四種會放入異步任務隊列:

1.setTimeout() 設置的異步延遲事件;

2.DOM 操做相關如佈局和繪製事件;

3.網絡 I/O 如 AJAX 請求事件;

4.用戶操做事件,如鼠標點擊、鍵盤敲擊。

 

上圖Event Loop大體描述就是:

  • 主線程運行時會產生執行棧,棧中的代碼調用某些api時,它們會在事件隊列中添加各類事件(當知足觸發條件後,如ajax請求完畢)
  • 而棧中的代碼執行完畢,就會讀取事件隊列中的事件,去執行那些回調
  • 如此循環
  • 注意,老是要等待棧中的代碼執行完畢後纔會去讀取事件隊列中的事件


下面這個例子很好闡釋事件循環:

setTimeout(function () {
      console.log('timeout 2222')
      alert('22222222')
    }, 2000)
    setTimeout(function () {
      console.log('timeout 1111')
      alert('1111111')
    }, 1000)
    setTimeout(function () {
      console.log('timeout() 00000')
    }, 0)//當指定的值小於 4 毫秒,則增長到 4ms(4ms 是 HTML5 標準指定的,對於 2010 年及以前的瀏覽器則是 10ms)
    function fn() {
      console.log('fn()')
    }
    fn()
    console.log('alert()以前')
    alert('------') //暫停當前主線程的執行, 同時暫停計時, 點擊肯定後, 恢復程序執行和計時
    console.log('alert()以後')

有兩點咱們須要注意下:

  • 定時器零延遲(setTimeout(func, 0))並非意味着回調函數馬上執行。至少4ms,纔會執行回調函數。它取決於主線程當前是否空閒與「任務隊列」裏其前面正在等待的任務。
  • 只有在到達指定時間時,定時器就會將相應回調函數插入「任務隊列」尾部

總結:異步任務(各類瀏覽器事件、定時器和Ajax等)都是先添加到「任務隊列」(定時器則到達其指定參數時)。當 Stack 棧(JavaScript 主線程)爲空時,就會讀取 Queue 隊列(任務隊列)的第一個任務(隊首),最後執行。

補充1:

放入異步任務隊列的時機,咱們經過 setTimeout的例子來詳細說明:

  <script>
    for(var i=0;i<5;i++){
      setTimeout(function(){
        console.log(i)
      },1000)
    }         //結果輸出5個5
  </script>

for循環一次碰到一個 setTimeout(),並非立刻把setTimeout()拿到異步隊列中,而要等到一秒後,纔將其放到任務隊列裏面,一旦"執行棧"中的全部同步任務執行完畢(即for循環結束,此時i已經爲5),系統就會讀取已經存放"任務隊列"的setTimeout()(有五個),因而答案是輸出5個5。

由於 for 循環會先執行完(同步優先於異步優先於回調),這時五個 setTimeout 的回調所有塞入了事件隊列中,而後 1 秒後一塊兒執行了。

由於 setTimeout 的 console.log(i); 的i是 var 定義的,因此是函數級的做用域,不屬於 for 循環體,屬於 global。等到 for 循環結束,i 已經等於 5 了,這個時候再執行 setTimeout 的五個回調函數(參考上面對事件機制的闡述),裏面的 console.log(i); 的 i 去向上找做用域,只能找到 global下 的 i,即 5。因此輸出都是 5。

解決辦法:人爲給 console.log(i); 創造做用域,保存i的值。

解決方法1:使用let

  <script>
    for(let i=0;i<5;i++){
      setTimeout(function(){
        console.log(i)
      },1000)
    }         
  </script>

let 爲代碼塊的做用域,因此每一次 for 循環,console.log(i); 都引用到 for 代碼塊做用域下的i,由於這樣被引用,因此 for 循環結束後,這些做用域在 setTimeout 未執行前都不會被釋放。

 

解決方法2:使用當即執行函數

  <script>
    for(var i=0;i<5;i++){
      (function(i){
        setTimeout(function(){
          console.log(i)
        },1000)
      })(i)
    }    
  </script>

裏用到馬上執行函數。這樣 console.log(i); 中的i就保存在每一次循環生成的馬上執行函數中的做用域裏了。

解決方法3:閉包

for(var i = 1;i < 5;i++){  
  var a = function(){  
      var j = i;    
    setTimeout(function(){  
          console.log(j);  
      },1000)  
  }    
a();
}

 補充2:

以異步AJAX爲例,說明Event loop執行機制

假設存在以下的代碼

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

那麼,

再次:

主線程在發起AJAX請求後,會繼續執行其餘代碼。AJAX線程負責請求segmentfault.com,拿到響應後,它會把響應封裝成一個JavaScript對象,往任務隊列裏添加一個事件:

// 任務隊列中的事件可想象成函數 var message = function () {    callbackFn(response); }

其中的callbackFn就是前面代碼中獲得成功響應時的回調函數。主線程在執行完當前循環中的全部代碼後,就會到任務隊列取出這個事件(也就是message函數),並執行它。到此爲止,就完成了工做線程對主線程的通知,回調函數也就獲得了執行。

用圖表示這個過程就是:

image 

5、微任務(Microtask)與宏任務(Macrotask)

咱們上面提到異步任務分爲宏任務和微任務,宏任務隊列能夠有多個,微任務隊列只有一個

  • 宏任務包括:script(全局任務), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • 微任務包括: new Promise().then(回調), process.nextTick, Object.observe(已廢棄), MutationObserver(html5新特性)

當執行棧中的全部同步任務執行完畢時,是先執行宏任務仍是微任務呢?

  • 因爲執行代碼入口都是全局任務 script,而全局任務屬於宏任務,因此當棧爲空,同步任務任務執行完畢時,會先執行微任務隊列裏的任務。
  • 微任務隊列裏的任務所有執行完畢後,會讀取宏任務隊列中拍最前的任務。
  • 執行宏任務的過程當中,遇到微任務,依次加入微任務隊列。
  • 棧空後,再次讀取微任務隊列裏的任務,依次類推。

一句話歸納上面的流程圖:當某個宏任務隊列的中的任務所有執行完之後,會查看是否有微任務隊列。若是有,先執行微任務隊列中的全部任務,若是沒有,就查看是否有其餘宏任務隊列

 

 

接下來咱們看兩道例子來介紹上面流程:

Promise.resolve().then(()=>{ console.log('Promise1') setTimeout(()=>{ console.log('setTimeout2') },0) }) setTimeout(()=>{ console.log('setTimeout1') Promise.resolve().then(()=>{ console.log('Promise2') }) },0)

最後輸出結果是Promise1,setTimeout1,Promise2,setTimeout2

  • 一開始執行棧的同步任務執行完畢,會去查看是否有微任務隊列,上題中存在(有且只有一個),而後執行微任務隊列中的全部任務輸出Promise1,同時會生成一個宏任務 setTimeout2
  • 而後去查看宏任務隊列,宏任務 setTimeout1 在 setTimeout2 以前,先執行宏任務 setTimeout1,輸出 setTimeout1
  • 在執行宏任務setTimeout1時會生成微任務Promise2 ,放入微任務隊列中,接着先去清空微任務隊列中的全部任務,輸出 Promise2
  • 清空完微任務隊列中的全部任務後,就又會去宏任務隊列取一個,這回執行的是 setTimeout2
console.log('----------------- start -----------------'); setTimeout(() => { console.log('setTimeout'); }, 0) new Promise((resolve, reject) =>{ for (var i = 0; i < 5; i++) { console.log(i); } resolve(); // 修改promise實例對象的狀態爲成功的狀態 }).then(() => { console.log('promise實例成功回調執行'); }) console.log('----------------- end -----------------');

將上題調換順序

 

 

 

6、H5 Web Workers(多線程)

1. Web Workers的做用

正如上面所提到,JavaScript是單線程。當一個頁面加載一個複雜運算的 js 文件時,用戶界面可能會短暫地「凍結」,不能再作其餘操做。好比下面這個例子:

<input type="text" placeholder="數值" id="number">
<button id="btn">計算</button>
<script type="text/javascript">
  // 1 1 2 3 5 8    f(n) = f(n-1) + f(n-2)
  function fibonacci(n) {
    return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2)  //遞歸調用
  }
  var input = document.getElementById('number')
  document.getElementById('btn').onclick = function () {
    var number = input.value
    var result = fibonacci(number)
    alert(result)
  }
</script>


很顯然遇到這種頁面堵塞狀況,很影響用戶體驗的,有沒有啥辦法能夠改進這種情形?----Web Worker就應運而生了!

Web Worker 的做用,就是爲 JavaScript 創造多線程環境,容許主線程建立 Worker 線程,將一些任務分配給後者運行。在主線程運行的同時,Worker 線程在後臺運行,二者互不干擾。等到 Worker 線程完成計算任務,再把結果返回給主線程。這樣的好處是,一些計算密集型或高延遲的任務,被 Worker 線程負擔了,主線程(一般負責 UI 交互)就會很流暢,不會被阻塞或拖慢。其原理圖以下:

2. Web Workers的基本使用

主線程

  • 首先主線程採用new命令,調用Worker()構造函數,新建一個 Worker 線程
var worker = new Worker('work.js');
  • 而後主線程調用worker.postMessage()方法,向 Worker 發消息。
  • 接着,主線程經過worker.onmessage指定監聽函數,接收子線程發回來的消息。
var input = document.getElementById('number')
  document.getElementById('btn').onclick = function () {
    var number = input.value
    //建立一個Worker對象
    var worker = new Worker('worker.js')
    // 綁定接收消息的監聽
    worker.onmessage = function (event) {
      console.log('主線程接收分線程返回的數據: '+event.data)
      alert(event.data)
    }
    // 向分線程發送消息
    worker.postMessage(number)
    console.log('主線程向分線程發送數據: '+number)
  }
    console.log(this) // window

Worker 線程

  • Worker 線程內部須要有一個監聽函數,監聽message事件。
  • 經過 postMessage(data) 方法來向主線程發送數據。
//worker.js文件
function fibonacci(n) {
  return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2)  //遞歸調用
}
console.log(this)//[object DedicatedWorkerGlobalScope]
this.onmessage = function (event) {
  var number = event.data
  console.log('分線程接收到主線程發送的數據: '+number)
  //計算
  var result = fibonacci(number)
  postMessage(result)
  console.log('分線程向主線程返回數據: '+result)
  // alert(result)  alert是window的方法, 在分線程不能調用
  // 分線程中的全局對象再也不是window, 因此在分線程中不可能更新界面
}

這樣當分線程在計算時,用戶界面還能夠操做,並且更早拿到計算後數據,響應速度更快了。

3. Web Workers的缺點

  • 不能跨域加載JS
  • worker內代碼不能訪問DOM(更新UI)
  • 不是每一個瀏覽器都支持這個新特性(本文例子只能在Firefox瀏覽器上運行,chrome不支持)

若是須要源代碼,請猛戳Web Workers

若是以爲文章對你有些許幫助,歡迎在個人GitHub博客點贊和關注,感激涕零!

 

7、Node 中的 Event Loop

1.Node簡介

Node 中的 Event Loop 和瀏覽器中Event Loop的是徹底不相同的東西。Node.js採用V8做爲js的解析引擎,而I/O處理方面使用了本身設計的libuv,libuv是一個基於事件驅動的跨平臺抽象層,封裝了不一樣操做系統一些底層特性,對外提供統一的API,事件循環機制也是它裏面的實現(下文會詳細介紹)。

 

 Node.js的運行機制以下:

 

  • V8引擎解析JavaScript腳本。
  • 解析後的代碼,調用Node API。
  • libuv庫負責Node API的執行。它將不一樣的任務分配給不一樣的線程,造成一個Event Loop(事件循環),以異步的方式將任務的執行結果返回給V8引擎。
  • V8引擎再將結果返回給用戶。

2.六個階段

其中libuv引擎中的事件循環分爲 6 個階段,它們會按照順序反覆運行。每當進入某一個階段的時候,都會從對應的回調隊列中取出函數去執行。當隊列爲空或者執行的回調函數數量到達系統設定的閾值,就會進入下一階段。

 

 

從上圖中,大體看出node中的事件循環的順序:

外部輸入數據-->輪詢階段(poll)-->檢查階段(check)-->關閉事件回調階段(close callback)-->定時器檢測階段(timer)-->I/O事件回調階段(I/O callbacks)-->閒置階段(idle, prepare)-->輪詢階段(按照該順序反覆運行)...

  • timers 階段:這個階段執行timer(setTimeout、setInterval)的回調
  • I/O callbacks 階段:處理一些上一輪循環中的少數未執行的 I/O 回調
  • idle, prepare 階段:僅node內部使用
  • poll 階段:獲取新的I/O事件, 適當的條件下node將阻塞在這裏
  • check 階段:執行 setImmediate() 的回調
  • close callbacks 階段:執行 socket 的 close 事件回調

注意:上面六個階段都不包括 process.nextTick()(下文會介紹)

接下去咱們詳細介紹timerspollcheck這3個階段,由於平常開發中的絕大部分異步任務都是在這3個階段處理的。

(1) timer

timers 階段會執行 setTimeout 和 setInterval 回調,而且是由 poll 階段控制的。 一樣,在 Node 中定時器指定的時間也不是準確時間,只能是儘快執行

(2) poll

poll 是一個相當重要的階段,這一階段中,系統會作兩件事情

1.回到 timer 階段執行回調

2.執行 I/O 回調

而且在進入該階段時若是沒有設定了 timer 的話,會發生如下兩件事情

  • 若是 poll 隊列不爲空,會遍歷回調隊列並同步執行,直到隊列爲空或者達到系統限制
  • 若是 poll 隊列爲空時,會有兩件事發生
    • 若是有 setImmediate 回調須要執行,poll 階段會中止而且進入到 check 階段執行回調
    • 若是沒有 setImmediate 回調須要執行,會等待回調被加入到隊列中並當即執行回調,這裏一樣會有個超時時間設置防止一直等待下去

固然設定了 timer 的話且 poll 隊列爲空,則會判斷是否有 timer 超時,若是有的話會回到 timer 階段執行回調。

(3) check階段

setImmediate()的回調會被加入check隊列中,從event loop的階段圖能夠知道,check階段的執行順序在poll階段以後。 咱們先來看個例子:

console.log('start') setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(() => { console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) Promise.resolve().then(function() { console.log('promise3') }) console.log('end') //start=>end=>promise3=>timer1=>timer2=>promise1=>promise2 複製代碼
  • 一開始執行棧的同步任務(這屬於宏任務)執行完畢後(依次打印出start end,並將2個timer依次放入timer隊列),會先去執行微任務(這點跟瀏覽器端的同樣),因此打印出promise3
  • 而後進入timers階段,執行timer1的回調函數,打印timer1,並將promise.then回調放入microtask隊列,一樣的步驟執行timer2,打印timer2;這點跟瀏覽器端相差比較大,timers階段有幾個setTimeout/setInterval都會依次執行,並不像瀏覽器端,每執行一個宏任務後就去執行一個微任務(關於Node與瀏覽器的 Event Loop 差別,下文還會詳細介紹)。

3.Micro-Task 與 Macro-Task

Node端事件循環中的異步隊列也是這兩種:macro(宏任務)隊列和 micro(微任務)隊列。

  • 常見的 macro-task 好比:setTimeout、setInterval、 setImmediate、script(總體代碼)、 I/O 操做等。
  • 常見的 micro-task 好比: process.nextTick、new Promise().then(回調)等。

4.注意點

(1) setTimeout 和 setImmediate

兩者很是類似,區別主要在於調用時機不一樣。

  • setImmediate 設計在poll階段完成時執行,即check階段;
  • setTimeout 設計在poll階段爲空閒時,且設定時間到達後執行,但它在timer階段執行
setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); }); 複製代碼
  • 對於以上代碼來講,setTimeout 可能執行在前,也可能執行在後。
  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的 進入事件循環也是須要成本的,若是在準備時候花費了大於 1ms 的時間,那麼在 timer 階段就會直接執行 setTimeout 回調
  • 若是準備時間花費小於 1ms,那麼就是 setImmediate 回調先執行了

但當兩者在異步i/o callback內部調用時,老是先執行setImmediate,再執行setTimeout

const fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0) setImmediate(() => { console.log('immediate') }) }) // immediate // timeout 複製代碼

在上述代碼中,setImmediate 永遠先執行。由於兩個代碼寫在 IO 回調中,IO 回調是在 poll 階段執行,當回調執行完畢後隊列爲空,發現存在 setImmediate 回調,因此就直接跳轉到 check 階段去執行回調了。

(2) process.nextTick

這個函數實際上是獨立於 Event Loop 以外的,它有一個本身的隊列,當每一個階段完成後,若是存在 nextTick 隊列,就會清空隊列中的全部回調函數,而且優先於其餘 microtask 執行。

setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') }) }) }) }) // nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1 複製代碼

5、Node與瀏覽器的 Event Loop 差別

瀏覽器環境下,microtask的任務隊列是每一個macrotask執行完以後執行。而在Node.js中,microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask隊列的任務

 

接下咱們經過一個例子來講明二者區別:

setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) 複製代碼

瀏覽器端運行結果:timer1=>promise1=>timer2=>promise2

瀏覽器端的處理過程以下:

 

 

Node端運行結果分兩種狀況:

  • 若是是node11版本一旦執行一個階段裏的一個宏任務(setTimeout,setInterval和setImmediate)就馬上執行微任務隊列,這就跟瀏覽器端運行一致,最後的結果爲timer1=>promise1=>timer2=>promise2
  • 若是是node10及其以前版本:要看第一個定時器執行完,第二個定時器是否在完成隊列中。
    • 若是是第二個定時器還未在完成隊列中,最後的結果爲timer1=>promise1=>timer2=>promise2
    • 若是是第二個定時器已經在完成隊列中,則最後的結果爲timer1=>timer2=>promise1=>promise2(下文過程解釋基於這種狀況下)

1.全局腳本(main())執行,將2個timer依次放入timer隊列,main()執行完畢,調用棧空閒,任務隊列開始執行;

2.首先進入timers階段,執行timer1的回調函數,打印timer1,並將promise1.then回調放入microtask隊列,一樣的步驟執行timer2,打印timer2;

3.至此,timer階段執行結束,event loop進入下一個階段以前,執行microtask隊列的全部任務,依次打印promise一、promise2

Node端的處理過程以下:

 

6、總結

瀏覽器和Node 環境下,microtask 任務隊列的執行時機不一樣

  • Node端,microtask 在事件循環的各個階段之間執行
  • 瀏覽器端,microtask 在事件循環的 macrotask 執行完以後執行

後記

文章於2019.1.16晚,對最後一個例子在node運行結果,從新修改!再次特別感謝zy445566的精彩點評,因爲node版本更新到11,Event Loop運行原理髮生了變化,一旦執行一個階段裏的一個宏任務(setTimeout,setInterval和setImmediate)就馬上執行微任務隊列,這點就跟瀏覽器端一致

參考文章



 

 

參考文章

進程和線程

進程與線程的一個簡單解釋

關於JavaScript單線程的一些事

JavaScript 運行機制詳解:再談Event Loop

Web Worker 是什麼鬼?

Web Worker 使用教程

 

 

 

前言

看法有限,若有描述不當之處,請幫忙及時指出,若有錯誤,會及時修正。

----------超長文+多圖預警,須要花費很多時間。----------

若是看完本文後,還對進程線程傻傻分不清,不清楚瀏覽器多進程、瀏覽器內核多線程、JS單線程、JS運行機制的區別。那麼請回復我,必定是我寫的還不夠清晰,我來改。。。

----------正文開始----------

最近發現有很多介紹JS單線程運行機制的文章,可是發現不少都僅僅是介紹某一部分的知識,並且各個地方的說法還不統一,容易形成困惑。
所以準備梳理這塊知識點,結合已有的認知,基於網上的大量參考資料,
從瀏覽器多進程到JS單線程,將JS引擎的運行機制系統的梳理一遍。

展示形式:因爲是屬於系統梳理型,就沒有由淺入深了,而是從頭至尾的梳理知識體系,
重點是將關鍵節點的知識點串聯起來,而不是僅僅剖析某一部分知識。

內容是:從瀏覽器進程,再到瀏覽器內核運行,再到JS引擎單線程,再到JS事件循環機制,從頭至尾系統的梳理一遍,擺脫碎片化,造成一個知識體系

目標是:看完這篇文章後,對瀏覽器多進程,JS單線程,JS事件循環機制這些都能有必定理解,
有一個知識體系骨架,而不是似懂非懂的感受。

另外,本文適合有必定經驗的前端人員,新手請規避,避免受到過多的概念衝擊。能夠先存起來,有了必定理解後再看,也能夠分紅多批次觀看,避免過分疲勞。

大綱

  • 區分進程和線程
  • 瀏覽器是多進程的

    • 瀏覽器都包含哪些進程?
    • 瀏覽器多進程的優點
    • 重點是瀏覽器內核(渲染進程)
    • Browser進程和瀏覽器內核(Renderer進程)的通訊過程
  • 梳理瀏覽器內核中線程之間的關係

    • GUI渲染線程與JS引擎線程互斥
    • JS阻塞頁面加載
    • WebWorker,JS的多線程?
    • WebWorker與SharedWorker
  • 簡單梳理下瀏覽器渲染流程

    • load事件與DOMContentLoaded事件的前後
    • css加載是否會阻塞dom樹渲染?
    • 普通圖層和複合圖層
  • 從Event Loop談JS的運行機制

    • 事件循環機制進一步補充
    • 單獨說說定時器
    • setTimeout而不是setInterval
  • 事件循環進階:macrotask與microtask
  • 寫在最後的話

區分進程和線程

線程和進程區分不清,是不少新手都會犯的錯誤,沒有關係。這很正常。先看看下面這個形象的比喻:

- 進程是一個工廠,工廠有它的獨立資源

- 工廠之間相互獨立

- 線程是工廠中的工人,多個工人協做完成任務

- 工廠內有一個或多個工人

- 工人之間共享空間

再完善完善概念:

- 工廠的資源 -> 系統分配的內存(獨立的一塊內存)

- 工廠之間的相互獨立 -> 進程之間相互獨立

- 多個工人協做完成任務 -> 多個線程在進程中協做完成任務

- 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成

- 工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)

而後再鞏固下:

若是是windows電腦中,能夠打開任務管理器,能夠看到有一個後臺進程列表。對,那裏就是查看進程的地方,並且能夠看到每一個進程的內存資源信息以及cpu佔有率。

因此,應該更容易理解了:進程是cpu資源分配的最小單位(系統會給它分配內存)

最後,再用較爲官方的術語描述一遍:

  • 進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)
  • 線程是cpu調度的最小單位(線程是創建在進程的基礎上的一次程序運行單位,一個進程中能夠有多個線程)

tips

  • 不一樣進程之間也能夠通訊,不過代價較大
  • 如今,通常通用的叫法:單線程與多線程,都是指在一個進程內的單和多。(因此核心仍是得屬於一個進程才行)

瀏覽器是多進程的

理解了進程與線程了區別後,接下來對瀏覽器進行必定程度上的認識:(先看下簡化理解)

  • 瀏覽器是多進程的
  • 瀏覽器之因此可以運行,是由於系統給它的進程分配了資源(cpu、內存)
  • 簡單點理解,每打開一個Tab頁,就至關於建立了一個獨立的瀏覽器進程。

關於以上幾點的驗證,請再第一張圖

圖中打開了Chrome瀏覽器的多個標籤頁,而後能夠在Chrome的任務管理器中看到有多個進程(分別是每個Tab頁面有一個獨立的進程,以及一個主進程)。
感興趣的能夠自行嘗試下,若是再多打開一個Tab頁,進程正常會+1以上

注意:在這裏瀏覽器應該也有本身的優化機制,有時候打開多個tab頁後,能夠在Chrome任務管理器中看到,有些進程被合併了
(因此每個Tab標籤對應一個進程並不必定是絕對的)

瀏覽器都包含哪些進程?

知道了瀏覽器是多進程後,再來看看它到底包含哪些進程:(爲了簡化理解,僅列舉主要進程)

  1. Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。做用有

    • 負責瀏覽器界面顯示,與用戶交互。如前進,後退等
    • 負責各個頁面的管理,建立和銷燬其餘進程
    • 將Renderer進程獲得的內存中的Bitmap,繪製到用戶界面上
    • 網絡資源的管理,下載等
  2. 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才建立
  3. GPU進程:最多一個,用於3D繪製等
  4. 瀏覽器渲染進程(瀏覽器內核)(Renderer進程,內部是多線程的):默認每一個Tab頁面一個進程,互不影響。主要做用爲

    • 頁面渲染,腳本執行,事件處理等

強化記憶:在瀏覽器中打開一個網頁至關於新起了一個進程(進程內有本身的多線程)

固然,瀏覽器有時會將多個進程合併(譬如打開多個空白標籤頁後,會發現多個空白標籤頁被合併成了一個進程),如圖

另外,能夠經過Chrome的更多工具 -> 任務管理器自行驗證

瀏覽器多進程的優點

相比於單進程瀏覽器,多進程有以下優勢:

  • 避免單個page crash影響整個瀏覽器
  • 避免第三方插件crash影響整個瀏覽器
  • 多進程充分利用多核優點
  • 方便使用沙盒模型隔離插件等進程,提升瀏覽器穩定性

簡單點理解:若是瀏覽器是單進程,那麼某個Tab頁崩潰了,就影響了整個瀏覽器,體驗有多差;同理若是是單進程,插件崩潰了也會影響整個瀏覽器;並且多進程還有其它的諸多優點。。。

固然,內存等資源消耗也會更大,有點空間換時間的意思。

重點是瀏覽器內核(渲染進程)

重點來了,咱們能夠看到,上面提到了這麼多的進程,那麼,對於普通的前端操做來講,最終要的是什麼呢?答案是渲染進程

能夠這樣理解,頁面的渲染,JS的執行,事件的循環,都在這個進程內進行。接下來重點分析這個進程

請牢記,瀏覽器的渲染進程是多線程的(這點若是不理解,請回頭看進程和線程的區分

終於到了線程這個概念了?,好親切。那麼接下來看看它都包含了哪些線程(列舉一些主要常駐線程):

  1. GUI渲染線程

    • 負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。
    • 當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時,該線程就會執行
    • 注意,GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(至關於被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行。
  2. JS引擎線程

    • 也稱爲JS內核,負責處理Javascript腳本程序。(例如V8引擎)
    • JS引擎線程負責解析Javascript腳本,運行代碼。
    • JS引擎一直等待着任務隊列中任務的到來,而後加以處理,一個Tab頁(renderer進程)中不管何時都只有一個JS線程在運行JS程序
    • 一樣注意,GUI渲染線程與JS引擎線程是互斥的,因此若是JS執行的時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞。
  3. 事件觸發線程

    • 歸屬於瀏覽器而不是JS引擎,用來控制事件循環(能夠理解,JS引擎本身都忙不過來,須要瀏覽器另開線程協助)
    • 當JS引擎執行代碼塊如setTimeOut時(也可來自瀏覽器內核的其餘線程,如鼠標點擊、AJAX異步請求等),會將對應任務添加到事件線程中
    • 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理
    • 注意,因爲JS的單線程關係,因此這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)

  4. 定時觸發器線程

    • 傳說中的setIntervalsetTimeout所在線程
    • 瀏覽器定時計數器並非由JavaScript引擎計數的,(由於JavaScript引擎是單線程的, 若是處於阻塞線程狀態就會影響記計時的準確)
    • 所以經過單獨線程來計時並觸發定時(計時完畢後,添加到事件隊列中,等待JS引擎空閒後執行)
    • 注意,W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算爲4ms。
  5. 異步http請求線程

    • 在XMLHttpRequest在鏈接後是經過瀏覽器新開一個線程請求
    • 將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中。再由JavaScript引擎執行。

看到這裏,若是以爲累了,能夠先休息下,這些概念須要被消化,畢竟後續將提到的事件循環機制就是基於事件觸發線程的,因此若是僅僅是看某個碎片化知識,
可能會有一種似懂非懂的感受。要完成的梳理一遍才能快速沉澱,不易遺忘。放張圖鞏固下吧:

再說一點,爲何JS引擎是單線程的?額,這個問題其實應該沒有標準答案,譬如,可能僅僅是由於因爲多線程的複雜性,譬如多線程操做通常要加鎖,所以最初設計時選擇了單線程。。。

Browser進程和瀏覽器內核(Renderer進程)的通訊過程

看到這裏,首先,應該對瀏覽器內的進程和線程都有必定理解了,那麼接下來,再談談瀏覽器的Browser進程(控制進程)是如何和內核通訊的,
這點也理解後,就能夠將這部分的知識串聯起來,從頭至尾有一個完整的概念。

若是本身打開任務管理器,而後打開一個瀏覽器,就能夠看到:任務管理器中出現了兩個進程(一個是主控進程,一個則是打開Tab頁的渲染進程)
而後在這前提下,看下整個的過程:(簡化了不少)

  • Browser進程收到用戶請求,首先須要獲取頁面內容(譬如經過網絡下載資源),隨後將該任務經過RendererHost接口傳遞給Render進程
  • Renderer進程的Renderer接口收到消息,簡單解釋後,交給渲染線程,而後開始渲染

    • 渲染線程接收請求,加載網頁並渲染網頁,這其中可能須要Browser進程獲取資源和須要GPU進程來幫助渲染
    • 固然可能會有JS線程操做DOM(這樣可能會形成迴流並重繪)
    • 最後Render進程將結果傳遞給Browser進程
  • Browser進程接收到結果並將結果繪製出來

這裏繪一張簡單的圖:(很簡化)

看完這一整套流程,應該對瀏覽器的運做有了必定理解了,這樣有了知識架構的基礎後,後續就方便往上填充內容。

這塊再往深處講的話就涉及到瀏覽器內核源碼解析了,不屬於本文範圍。

若是這一塊要深挖,建議去讀一些瀏覽器內核源碼解析文章,或者能夠先看看參考下來源中的第一篇文章,寫的不錯

梳理瀏覽器內核中線程之間的關係

到了這裏,已經對瀏覽器的運行有了一個總體的概念,接下來,先簡單梳理一些概念

GUI渲染線程與JS引擎線程互斥

因爲JavaScript是可操縱DOM的,若是在修改這些元素屬性同時渲染界面(即JS線程和UI線程同時運行),那麼渲染線程先後得到的元素數據就可能不一致了。

所以爲了防止渲染出現不可預期的結果,瀏覽器設置GUI渲染線程與JS引擎爲互斥的關係,當JS引擎執行時GUI線程會被掛起,
GUI更新則會被保存在一個隊列中等到JS引擎線程空閒時當即被執行。

JS阻塞頁面加載

從上述的互斥關係,能夠推導出,JS若是執行時間過長就會阻塞頁面。

譬如,假設JS引擎正在進行巨量的計算,此時就算GUI有更新,也會被保存到隊列中,等待JS引擎空閒後執行。
而後,因爲巨量計算,因此JS引擎極可能好久好久後才能空閒,天然會感受到巨卡無比。

因此,要儘可能避免JS執行時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞的感受。

WebWorker,JS的多線程?

前文中有提到JS引擎是單線程的,並且JS執行時間過長會阻塞頁面,那麼JS就真的對cpu密集型計算無能爲力麼?

因此,後來HTML5中支持了Web Worker

MDN的官方解釋是:

Web Worker爲Web內容在後臺線程中運行腳本提供了一種簡單的方法。線程能夠執行任務而不干擾用戶界面

一個worker是使用一個構造函數建立的一個對象(e.g. Worker()) 運行一個命名的JavaScript文件 

這個文件包含將在工做線程中運行的代碼; workers 運行在另外一個全局上下文中,不一樣於當前的window 所以,使用 window快捷方式獲取當前全局的範圍 (而不是self) 在一個 Worker 內將返回錯誤

這樣理解下:

  • 建立Worker時,JS引擎向瀏覽器申請開一個子線程(子線程是瀏覽器開的,徹底受主線程控制,並且不能操做DOM)
  • JS引擎線程與worker線程間經過特定的方式通訊(postMessage API,須要經過序列化對象來與線程交互特定的數據)

因此,若是有很是耗時的工做,請單獨開一個Worker線程,這樣裏面無論如何翻天覆地都不會影響JS引擎主線程,
只待計算出結果後,將結果通訊給主線程便可,perfect!

並且注意下,JS引擎是單線程的,這一點的本質仍然未改變,Worker能夠理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計算問題。

其它,關於Worker的詳解就不是本文的範疇了,所以再也不贅述。

WebWorker與SharedWorker

既然都到了這裏,就再提一下SharedWorker(避免後續將這兩個概念搞混)

  • WebWorker只屬於某個頁面,不會和其餘頁面的Render進程(瀏覽器內核進程)共享

    • 因此Chrome在Render進程中(每個Tab頁就是一個render進程)建立一個新的線程來運行Worker中的JavaScript程序。
  • SharedWorker是瀏覽器全部頁面共享的,不能採用與Worker一樣的方式實現,由於它不隸屬於某個Render進程,能夠爲多個Render進程共享使用

    • 因此Chrome瀏覽器爲SharedWorker單首創建一個進程來運行JavaScript程序,在瀏覽器中每一個相同的JavaScript只存在一個SharedWorker進程,無論它被建立多少次。

看到這裏,應該就很容易明白了,本質上就是進程和線程的區別。SharedWorker由獨立的進程管理,WebWorker只是屬於render進程下的一個線程

簡單梳理下瀏覽器渲染流程

原本是直接計劃開始談JS運行機制的,但想了想,既然上述都一直在談瀏覽器,直接跳到JS可能再突兀,所以,中間再補充下瀏覽器的渲染流程(簡單版本)

爲了簡化理解,前期工做直接省略成:(要展開的或徹底能夠寫另外一篇超長文)

- 瀏覽器輸入url,瀏覽器主進程接管,開一個下載線程,
而後進行 http請求(略去DNS查詢,IP尋址等等操做),而後等待響應,獲取內容,
隨後將內容經過RendererHost接口轉交給Renderer進程

- 瀏覽器渲染流程開始

瀏覽器器內核拿到內容後,渲染大概能夠劃分紅如下幾個步驟:

  1. 解析html創建dom樹
  2. 解析css構建render樹(將CSS代碼解析成樹形的數據結構,而後結合DOM合併成render樹)
  3. 佈局render樹(Layout/reflow),負責各元素尺寸、位置的計算
  4. 繪製render樹(paint),繪製頁面像素信息
  5. 瀏覽器會將各層的信息發送給GPU,GPU會將各層合成(composite),顯示在屏幕上。

全部詳細步驟都已經略去,渲染完畢後就是load事件了,以後就是本身的JS邏輯處理了

既然略去了一些詳細的步驟,那麼就提一些可能須要注意的細節把。

這裏重繪參考來源中的一張圖:(參考來源第一篇)

load事件與DOMContentLoaded事件的前後

上面提到,渲染完畢後會觸發load事件,那麼你能分清楚load事件與DOMContentLoaded事件的前後麼?

很簡單,知道它們的定義就能夠了:

  • 當 DOMContentLoaded 事件觸發時,僅當DOM加載完成,不包括樣式表,圖片。

(譬如若是有async加載的腳本就不必定完成)

  • 當 onload 事件觸發時,頁面上全部的DOM,樣式表,腳本,圖片都已經加載完成了。

(渲染完畢了)

因此,順序是:DOMContentLoaded -> load

css加載是否會阻塞dom樹渲染?

這裏說的是頭部引入css的狀況

首先,咱們都知道:css是由單獨的下載線程異步下載的。

而後再說下幾個現象:

  • css加載不會阻塞DOM樹解析(異步加載時DOM照常構建)
  • 但會阻塞render樹渲染(渲染時需等css加載完畢,由於render樹須要css信息)

這可能也是瀏覽器的一種優化機制。

由於你加載css的時候,可能會修改下面DOM節點的樣式,
若是css加載不阻塞render樹渲染的話,那麼當css加載完以後,
render樹可能又得從新重繪或者回流了,這就形成了一些沒有必要的損耗。
因此乾脆就先把DOM樹的結構先解析完,把能夠作的工做作完,而後等你css加載完以後,
在根據最終的樣式來渲染render樹,這種作法性能方面確實會比較好一點。

普通圖層和複合圖層

渲染步驟中就提到了composite概念。

能夠簡單的這樣理解,瀏覽器渲染的圖層通常包含兩大類:普通圖層以及複合圖層

首先,普通文檔流內能夠理解爲一個複合圖層(這裏稱爲默認複合層,裏面無論添加多少元素,其實都是在同一個複合圖層中)

其次,absolute佈局(fixed也同樣),雖然能夠脫離普通文檔流,但它仍然屬於默認複合層

而後,能夠經過硬件加速的方式,聲明一個新的複合圖層,它會單獨分配資源
(固然也會脫離普通文檔流,這樣一來,無論這個複合圖層中怎麼變化,也不會影響默認複合層裏的迴流重繪)

能夠簡單理解下:GPU中,各個複合圖層是單獨繪製的,因此互不影響,這也是爲何某些場景硬件加速效果一級棒

能夠Chrome源碼調試 -> More Tools -> Rendering -> Layer borders中看到,黃色的就是複合圖層信息

以下圖。能夠驗證上述的說法

如何變成複合圖層(硬件加速)

將該元素變成一個複合圖層,就是傳說中的硬件加速技術

  • 最經常使用的方式:translate3dtranslateZ
  • opacity屬性/過渡動畫(須要動畫執行的過程當中纔會建立合成層,動畫沒有開始或結束後元素還會回到以前的狀態)
  • will-chang屬性(這個比較偏僻),通常配合opacity與translate使用(並且經測試,除了上述能夠引起硬件加速的屬性外,其它屬性並不會變成複合層),

做用是提早告訴瀏覽器要變化,這樣瀏覽器會開始作一些優化工做(這個最好用完後就釋放)

  • <video><iframe><canvas><webgl>等元素
  • 其它,譬如之前的flash插件

absolute和硬件加速的區別

能夠看到,absolute雖然能夠脫離普通文檔流,可是沒法脫離默認複合層。
因此,就算absolute中信息改變時不會改變普通文檔流中render樹,
可是,瀏覽器最終繪製時,是整個複合層繪製的,因此absolute中信息的改變,仍然會影響整個複合層的繪製。
(瀏覽器會重繪它,若是複合層中內容多,absolute帶來的繪製信息變化過大,資源消耗是很是嚴重的)

而硬件加速直接就是在另外一個複合層了(另起爐竈),因此它的信息改變不會影響默認複合層
(固然了,內部確定會影響屬於本身的複合層),僅僅是引起最後的合成(輸出視圖)

複合圖層的做用?

通常一個元素開啓硬件加速後會變成複合圖層,能夠獨立於普通文檔流中,改動後能夠避免整個頁面重繪,提高性能

可是儘可能不要大量使用複合圖層,不然因爲資源消耗過分,頁面反而會變的更卡

硬件加速時請使用index

使用硬件加速時,儘量的使用index,防止瀏覽器默認給後續的元素建立複合層渲染

具體的原理時這樣的:
**webkit CSS3中,若是這個元素添加了硬件加速,而且index層級比較低,
那麼在這個元素的後面其它元素(層級比這個元素高的,或者相同的,而且releative或absolute屬性相同的),
會默認變爲複合層渲染,若是處理不當會極大的影響性能**

簡單點理解,其實能夠認爲是一個隱式合成的概念:若是a是一個複合圖層,並且b在a上面,那麼b也會被隱式轉爲一個複合圖層,這點須要特別注意

另外,這個問題能夠在這個地址看到重現(原做者分析的挺到位的,直接上連接):

http://web.jobbole.com/83575/

從Event Loop談JS的運行機制

到此時,已是屬於瀏覽器頁面初次渲染完畢後的事情,JS引擎的一些運行機制分析。

注意,這裏不談可執行上下文VOscop chain等概念(這些徹底能夠整理成另外一篇文章了),這裏主要是結合Event Loop來談JS代碼是如何執行的。

讀這部分的前提是已經知道了JS引擎是單線程,並且這裏會用到上文中的幾個概念:(若是不是很理解,能夠回頭溫習)

  • JS引擎線程
  • 事件觸發線程
  • 定時觸發器線程

而後再理解一個概念:

  • JS分爲同步任務和異步任務
  • 同步任務都在主線程上執行,造成一個執行棧
  • 主線程以外,事件觸發線程管理着一個任務隊列,只要異步任務有了運行結果,就在任務隊列之中放置一個事件。
  • 一旦執行棧中的全部同步任務執行完畢(此時JS引擎空閒),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中,開始執行。

看圖:

看到這裏,應該就能夠理解了:爲何有時候setTimeout推入的事件不能準時執行?由於可能在它推入到事件列表時,主線程還不空閒,正在執行其它代碼,
因此天然有偏差。

事件循環機制進一步補充

這裏就直接引用一張圖片來協助理解:(參考自Philip Roberts的演講《Help, I'm stuck in an event-loop》)

上圖大體描述就是:

  • 主線程運行時會產生執行棧,

棧中的代碼調用某些api時,它們會在事件隊列中添加各類事件(當知足觸發條件後,如ajax請求完畢)

  • 而棧中的代碼執行完畢,就會讀取事件隊列中的事件,去執行那些回調
  • 如此循環
  • 注意,老是要等待棧中的代碼執行完畢後纔會去讀取事件隊列中的事件

單獨說說定時器

上述事件循環機制的核心是:JS引擎線程和事件觸發線程

但事件上,裏面還有一些隱藏細節,譬如調用setTimeout後,是如何等待特定時間後才添加到事件隊列中的?

是JS引擎檢測的麼?固然不是了。它是由定時器線程控制(由於JS引擎本身都忙不過來,根本無暇分身)

爲何要單獨的定時器線程?由於JavaScript引擎是單線程的, 若是處於阻塞線程狀態就會影響記計時的準確,所以頗有必要單獨開一個線程用來計時。

何時會用到定時器線程?當使用setTimeoutsetInterval,它須要定時器線程計時,計時完成後就會將特定的事件推入事件隊列中。

譬如:

setTimeout(function(){ console.log('hello!'); }, 1000);

這段代碼的做用是當1000毫秒計時完畢後(由定時器線程計時),將回調函數推入事件隊列中,等待主線程執行

setTimeout(function(){ console.log('hello!'); }, 0); console.log('begin');

這段代碼的效果是最快的時間內將回調函數推入事件隊列中,等待主線程執行

注意:

  • 執行結果是:先beginhello!
  • 雖然代碼的本意是0毫秒後就推入事件隊列,可是W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算爲4ms。

(不過也有一說是不一樣瀏覽器有不一樣的最小時間設定)

  • 就算不等待4ms,就算假設0毫秒就推入事件隊列,也會先執行begin(由於只有可執行棧內空了後纔會主動讀取事件隊列)

setTimeout而不是setInterval

用setTimeout模擬按期計時和直接用setInterval是有區別的。

由於每次setTimeout計時到後就會去執行,而後執行一段時間後纔會繼續setTimeout,中間就多了偏差
(偏差多少與代碼執行時間有關)

而setInterval則是每次都精確的隔一段時間推入一個事件
(可是,事件的實際執行時間不必定就準確,還有多是這個事件還沒執行完畢,下一個事件就來了)

並且setInterval有一些比較致命的問題就是:

  • 累計效應(上面提到的),若是setInterval代碼在(setInterval)再次添加到隊列以前尚未完成執行,

就會致使定時器代碼連續運行好幾回,而之間沒有間隔。
就算正常間隔執行,多個setInterval的代碼執行時間可能會比預期小(由於代碼執行須要必定時間)

  • 譬如像iOS的webview,或者Safari等瀏覽器中都有一個特色,在滾動的時候是不執行JS的,若是使用了setInterval,會發如今滾動結束後會執行屢次因爲滾動不執行JS積攢回調,若是回調執行時間過長,就會很是容器形成卡頓問題和一些不可知的錯誤(這一塊後續有補充,setInterval自帶的優化,不會重複添加回調)
  • 並且把瀏覽器最小化顯示等操做時,setInterval並非不執行程序,

它會把setInterval的回調函數放在隊列中,等瀏覽器窗口再次打開時,一瞬間所有執行時

因此,鑑於這麼多但問題,目前通常認爲的最佳方案是:用setTimeout模擬setInterval,或者特殊場合直接用requestAnimationFrame

補充:JS高程中有提到,JS引擎會對setInterval進行優化,若是當前事件隊列中有setInterval的回調,不會重複添加。不過,仍然是有不少問題。。。

事件循環進階:macrotask與microtask

這段參考了參考來源中的第2篇文章(英文版的),(加了下本身的理解從新描述了下),
強烈推薦有英文基礎的同窗直接觀看原文,做者描述的很清晰,示例也很不錯,以下:

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

上文中將JS事件循環機制梳理了一遍,在ES5的狀況是夠用了,可是在ES6盛行的如今,仍然會遇到一些問題,譬以下面這題:

console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');

嗯哼,它的正確執行順序是這樣子的:

script start
script end
promise1
promise2
setTimeout

爲何呢?由於Promise裏有了一個一個新的概念:microtask

或者,進一步,JS中分爲兩種任務類型:macrotaskmicrotask,在ECMAScript中,microtask稱爲jobs,macrotask可稱爲task

它們的定義?區別?簡單點能夠按以下理解:

  • macrotask(又稱之爲宏任務),能夠理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)

    • 每個task會從頭至尾將這個任務執行完畢,不會執行其它
    • 瀏覽器爲了可以使得JS內部task與DOM任務可以有序的執行,會在一個task執行結束後,在下一個 task 執行開始前,對頁面進行從新渲染
(`task->渲染->task->...`)
  • microtask(又稱爲微任務),能夠理解是在當前 task 執行結束後當即執行的任務

    • 也就是說,在當前task任務後,下一個task以前,在渲染以前
    • 因此它的響應速度相比setTimeout(setTimeout是task)會更快,由於無需等渲染
    • 也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的全部microtask都執行完畢(在渲染前)

分別很麼樣的場景會造成macrotask和microtask呢?

  • macrotask:主代碼塊,setTimeout,setInterval等(能夠看到,事件隊列中的每個事件都是一個macrotask)
  • microtask:Promise,process.nextTick等

__補充:在node環境下,process.nextTick的優先級高於Promise__,也就是能夠簡單理解爲:在宏任務結束後會先執行微任務隊列中的nextTickQueue部分,而後纔會執行微任務中的Promise部分。

參考:https://segmentfault.com/q/1010000011914016

再根據線程來理解下:

  • macrotask中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發線程維護
  • microtask中的全部微任務都是添加到微任務隊列(Job Queues)中,等待當前macrotask執行完畢後執行,而這個隊列由JS引擎線程維護

(這點由本身理解+推測得出,由於它是在主線程下無縫執行的)

因此,總結下運行機制:

  • 執行一個宏任務(棧中沒有就從事件隊列中獲取)
  • 執行過程當中若是遇到微任務,就將它添加到微任務的任務隊列中
  • 宏任務執行完畢後,當即執行當前微任務隊列中的全部微任務(依次執行)
  • 當前宏任務執行完畢,開始檢查渲染,而後GUI線程接管渲染
  • 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)

如圖:

另外,請注意下Promisepolyfill與官方版本的區別:

  • 官方版本中,是標準的microtask形式
  • polyfill,通常都是經過setTimeout模擬的,因此是macrotask形式
  • 請特別注意這兩點區別

注意,有一些瀏覽器執行結果不同(由於它們可能把microtask當成macrotask來執行了),
可是爲了簡單,這裏不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能並不標準)

20180126補充:使用MutationObserver實現microtask

MutationObserver能夠用來實現microtask
(它屬於microtask,優先級小於Promise,
通常是Promise不支持時纔會這樣作)

它是HTML5中的新特性,做用是:監聽一個DOM變更,
當DOM對象樹發生任何變更時,Mutation Observer會獲得通知

像之前的Vue源碼中就是利用它來模擬nextTick的,
具體原理是,建立一個TextNode並監聽內容變化,
而後要nextTick的時候去改一下這個節點的文本內容,
以下:(Vue的源碼,未修改)

var counter = 1 var observer = new MutationObserver(nextTickHandler) var textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) }

對應Vue源碼連接

不過,如今的Vue(2.5+)的nextTick實現移除了MutationObserver的方式(聽說是兼容性緣由),
取而代之的是使用MessageChannel
(固然,默認狀況仍然是Promise,不支持才兼容的)。

MessageChannel屬於宏任務,優先級是:MessageChannel->setTimeout
因此Vue(2.5+)內部的nextTick與2.4及以前的實現是不同的,須要注意下。

這裏不展開,能夠看下http://www.javashuo.com/article/p-uvlubpli-gp.html

寫在最後的話

看到這裏,不知道對JS的運行機制是否是更加理解了,從頭至尾梳理,而不是就某一個碎片化知識應該是會更清晰的吧?

同時,也應該注意到了JS根本就沒有想象的那麼簡單,前端的知識也是無窮無盡,層出不窮的概念、N多易忘的知識點、各式各樣的框架、
底層原理方面也是能夠無限的往下深挖,而後你就會發現,你知道的太少了。。。

另外,本文也打算先告一段落,其它的,如JS詞法解析,可執行上下文以及VO等概念就不繼續在本文中寫了,後續能夠考慮另開新的文章。

最後,喜歡的話,就請給個贊吧!

附錄

博客

初次發佈2018.01.21於我我的博客上面

http://www.dailichun.com/2018/01/21/js_singlethread_eventloop.html

參考資料

 

 

 

 

 

 

 

 

 

 

 

 

 

一次弄懂Event Loop(完全解決此類面試問題)

前言

Event Loop即事件循環,是指瀏覽器或Node的一種解決javaScript單線程運行時不會阻塞的一種機制,也就是咱們常用異步的原理。

爲啥要弄懂Event Loop

  • 是要增長本身技術的深度,也就是懂得JavaScript的運行機制。

  • 如今在前端領域各類技術層出不窮,掌握底層原理,可讓本身以不變,應萬變。

  • 應對各大互聯網公司的面試,懂其原理,題目任其發揮。

堆,棧、隊列

 

 

堆(Heap)

堆是一種數據結構,是利用徹底二叉樹維護的一組數據,堆分爲兩種,一種爲最大堆,一種爲最小堆,將根節點最大的堆叫作最大堆或大根堆,根節點最小的堆叫作最小堆或小根堆。
堆是線性數據結構,至關於一維數組,有惟一後繼。

如最大堆

 

 

棧(Stack)

棧在計算機科學中是限定僅在表尾進行插入或刪除操做的線性表。 棧是一種數據結構,它按照後進先出的原則存儲數據,先進入的數據被壓入棧底,最後的數據在棧頂,須要讀數據的時候從棧頂開始彈出數據。
棧是隻能在某一端插入和刪除的特殊線性表。

 

 

隊列(Queue)

特殊之處在於它只容許在表的前端(front)進行刪除操做,而在表的後端(rear)進行插入操做,和棧同樣,隊列是一種操做受限制的線性表。
進行插入操做的端稱爲隊尾,進行刪除操做的端稱爲隊頭。 隊列中沒有元素時,稱爲空隊列。

隊列的數據元素又稱爲隊列元素。在隊列中插入一個隊列元素稱爲入隊,從隊列中刪除一個隊列元素稱爲出隊。由於隊列只容許在一端插入,在另外一端刪除,因此只有最先進入隊列的元素才能最早從隊列中刪除,故隊列又稱爲先進先出(FIFO—first in first out

 

 

Event Loop

JavaScript中,任務被分爲兩種,一種宏任務(MacroTask)也叫Task,一種叫微任務(MicroTask)。

MacroTask(宏任務)

  • script所有代碼、setTimeoutsetIntervalsetImmediate(瀏覽器暫時不支持,只有IE10支持,具體可見MDN)、I/OUI Rendering

MicroTask(微任務)

  • Process.nextTick(Node獨有)PromiseObject.observe(廢棄)MutationObserver(具體使用方式查看這裏

瀏覽器中的Event Loop

Javascript 有一個 main thread 主線程和 call-stack 調用棧(執行棧),全部的任務都會被放到調用棧等待主線程執行。

JS調用棧

JS調用棧採用的是後進先出的規則,當函數執行的時候,會被添加到棧的頂部,當執行棧執行完成後,就會從棧頂移出,直到棧內被清空。

同步任務和異步任務

Javascript單線程任務被分爲同步任務和異步任務,同步任務會在調用棧中按照順序等待主線程依次執行,異步任務會在異步任務有告終果後,將註冊的回調函數放入任務隊列中等待主線程空閒的時候(調用棧被清空),被讀取到棧內等待主線程的執行。

 

任務隊列 Task Queue,即隊列,是一種先進先出的一種數據結構。

 

 

 

事件循環的進程模型

  • 選擇當前要執行的任務隊列,選擇任務隊列中最早進入的任務,若是任務隊列爲空即null,則執行跳轉到微任務(MicroTask)的執行步驟。
  • 將事件循環中的任務設置爲已選擇任務。
  • 執行任務。
  • 將事件循環中當前運行任務設置爲null。
  • 將已經運行完成的任務從任務隊列中刪除。
  • microtasks步驟:進入microtask檢查點。
  • 更新界面渲染。
  • 返回第一步。

執行進入microtask檢查點時,用戶代理會執行如下步驟:

  • 設置microtask檢查點標誌爲true。
  • 當事件循環microtask執行不爲空時:選擇一個最早進入的microtask隊列的microtask,將事件循環的microtask設置爲已選擇的microtask,運行microtask,將已經執行完成的microtasknull,移出microtask中的microtask
  • 清理IndexDB事務
  • 設置進入microtask檢查點的標誌爲false。

上述可能不太好理解,下圖是我作的一張圖片。

 

 

執行棧在執行完同步任務後,查看執行棧是否爲空,若是執行棧爲空,就會去檢查微任務(microTask)隊列是否爲空,若是爲空的話,就執行Task(宏任務),不然就一次性執行完全部微任務。
每次單個宏任務執行完畢後,檢查微任務(microTask)隊列是否爲空,若是不爲空的話,會按照先入先出的規則所有執行完微任務(microTask)後,設置微任務(microTask)隊列爲null,而後再執行宏任務,如此循環。

舉個例子

console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end'); 複製代碼

首先咱們劃分幾個分類:

第一次執行:

Tasks:run script、 setTimeout callback

Microtasks:Promise then JS stack: script Log: script start、script end。 複製代碼

執行同步代碼,將宏任務(Tasks)和微任務(Microtasks)劃分到各自隊列中。

第二次執行:

Tasks:run script、 setTimeout callback

Microtasks:Promise2 then	

JS stack: Promise2 callback	
Log: script start、script end、promise一、promise2
複製代碼

執行宏任務後,檢測到微任務(Microtasks)隊列中不爲空,執行Promise1,執行完成Promise1後,調用Promise2.then,放入微任務(Microtasks)隊列中,再執行Promise2.then

第三次執行:

Tasks:setTimeout callback

Microtasks:	

JS stack: setTimeout callback
Log: script start、script end、promise一、promise二、setTimeout
複製代碼

當微任務(Microtasks)隊列中爲空時,執行宏任務(Tasks),執行setTimeout callback,打印日誌。

第四次執行:

Tasks:setTimeout callback

Microtasks:	

JS stack: 
Log: script start、script end、promise一、promise二、setTimeout
複製代碼

清空Tasks隊列和JS stack

以上執行幀動畫能夠查看Tasks, microtasks, queues and schedules
或許這張圖也更好理解些。

 

 

再舉個例子

console.log('script start') async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') } async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') }) console.log('script end') 複製代碼

這裏須要先理解async/await

async/await 在底層轉換成了 promise 和 then 回調函數。
也就是說,這是 promise 的語法糖。
每次咱們使用 await, 解釋器都建立一個 promise 對象,而後把剩下的 async 函數中的操做放到 then 回調函數中。
async/await 的實現,離不開 Promise。從字面意思來理解,async 是「異步」的簡寫,而 await 是 async wait 的簡寫能夠認爲是等待異步方法執行完成。

關於73如下版本和73版本的區別

  • 在老版本版本如下,先執行promise1promise2,再執行async1
  • 在73版本,先執行async1再執行promise1promise2

主要緣由是由於在谷歌(金絲雀)73版本中更改了規範,以下圖所示:

 

 

  • 區別在於RESOLVE(thenable)和之間的區別Promise.resolve(thenable)

在老版本中

  • 首先,傳遞給 await 的值被包裹在一個 Promise 中。而後,處理程序附加到這個包裝的 Promise,以便在 Promise 變爲 fulfilled 後恢復該函數,而且暫停執行異步函數,一旦 promise 變爲 fulfilled,恢復異步函數的執行。
  • 每一個 await 引擎必須建立兩個額外的 Promise(即便右側已是一個 Promise)而且它須要至少三個 microtask 隊列 tickstick爲系統的相對時間單位,也被稱爲系統的時基,來源於定時器的週期性中斷(輸出脈衝),一次中斷表示一個tick,也被稱作一個「時鐘滴答」、時標。)。

引用賀老師知乎上的一個例子

async function f() { await p console.log('ok') } 複製代碼

簡化理解爲:

function f() { return RESOLVE(p).then(() => { console.log('ok') }) } 複製代碼
  • 若是 RESOLVE(p) 對於 p 爲 promise 直接返回 p 的話,那麼 p的 then 方法就會被立刻調用,其回調就當即進入 job 隊列。
  • 而若是 RESOLVE(p) 嚴格按照標準,應該是產生一個新的 promise,儘管該 promise肯定會 resolve 爲 p,但這個過程自己是異步的,也就是如今進入 job 隊列的是新 promise 的 resolve過程,因此該 promise 的 then 不會被當即調用,而要等到當前 job 隊列執行到前述 resolve 過程纔會被調用,而後其回調(也就是繼續 await 以後的語句)才加入 job隊列,因此時序上就晚了。

谷歌(金絲雀)73版本中

  • 使用對PromiseResolve的調用來更改await的語義,以減小在公共awaitPromise狀況下的轉換次數。
  • 若是傳遞給 await 的值已是一個 Promise,那麼這種優化避免了再次建立 Promise 包裝器,在這種狀況下,咱們從最少三個 microtick 到只有一個 microtick

詳細過程:

73如下版本

  • 首先,打印script start,調用async1()時,返回一個Promise,因此打印出來async2 end
  • 每一個 await,會新產生一個promise,但這個過程自己是異步的,因此該await後面不會當即調用。
  • 繼續執行同步代碼,打印Promisescript end,將then函數放入微任務隊列中等待執行。
  • 同步執行完成以後,檢查微任務隊列是否爲null,而後按照先入先出規則,依次執行。
  • 而後先執行打印promise1,此時then的回調函數返回undefinde,此時又有then的鏈式調用,又放入微任務隊列中,再次打印promise2
  • 再回到await的位置執行返回的 Promise 的 resolve 函數,這又會把 resolve 丟到微任務隊列中,打印async1 end
  • 當微任務隊列爲空時,執行宏任務,打印setTimeout

谷歌(金絲雀73版本)

  • 若是傳遞給 await 的值已是一個 Promise,那麼這種優化避免了再次建立 Promise 包裝器,在這種狀況下,咱們從最少三個 microtick 到只有一個 microtick
  • 引擎再也不須要爲 await 創造 throwaway Promise - 在絕大部分時間。
  • 如今 promise 指向了同一個 Promise,因此這個步驟什麼也不須要作。而後引擎繼續像之前同樣,建立 throwaway Promise,安排 PromiseReactionJob 在 microtask 隊列的下一個 tick 上恢復異步函數,暫停執行該函數,而後返回給調用者。

具體詳情查看(這裏)。

NodeJS的Event Loop

 

 

Node中的Event Loop是基於libuv實現的,而libuv是 Node 的新跨平臺抽象層,libuv使用異步,事件驅動的編程方式,核心是提供i/o的事件循環和異步回調。libuv的API包含有時間,非阻塞的網絡,異步文件操做,子進程等等。 Event Loop就是在libuv中實現的。

 

 

NodeEvent loop一共分爲6個階段,每一個細節具體以下:

  • timers: 執行setTimeoutsetInterval中到期的callback
  • pending callback: 上一輪循環中少數的callback會放在這一階段執行。
  • idle, prepare: 僅在內部使用。
  • poll: 最重要的階段,執行pending callback,在適當的狀況下回阻塞在這個階段。
  • check: 執行setImmediate(setImmediate()是將事件插入到事件隊列尾部,主線程和事件隊列的函數執行完成以後當即執行setImmediate指定的回調函數)的callback
  • close callbacks: 執行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)

具體細節以下:

timers

執行setTimeoutsetInterval中到期的callback,執行這二者回調須要設置一個毫秒數,理論上來講,應該是時間一到就當即執行callback回調,可是因爲system的調度可能會延時,達不到預期時間。
如下是官網文檔解釋的例子:

const fs = require('fs'); function someAsyncOperation(callback) { // Assume this takes 95ms to complete fs.readFile('/path/to/file', callback); } const timeoutScheduled = Date.now(); setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms have passed since I was scheduled`); }, 100); // do someAsyncOperation which takes 95 ms to complete someAsyncOperation(() => { const startCallback = Date.now(); // do something that will take 10ms... while (Date.now() - startCallback < 10) { // do nothing } }); 複製代碼

當進入事件循環時,它有一個空隊列(fs.readFile()還沒有完成),所以定時器將等待剩餘毫秒數,當到達95ms時,fs.readFile()完成讀取文件而且其完成須要10毫秒的回調被添加到輪詢隊列並執行。
當回調結束時,隊列中再也不有回調,所以事件循環將看到已達到最快定時器的閾值,而後回到timers階段以執行定時器的回調。

在此示例中,您將看到正在調度的計時器與正在執行的回調之間的總延遲將爲105毫秒。

如下是我測試時間:

 

pending callbacks

此階段執行某些系統操做(例如TCP錯誤類型)的回調。 例如,若是TCP socket ECONNREFUSED在嘗試connect時receives,則某些* nix系統但願等待報告錯誤。 這將在pending callbacks階段執行。

poll

該poll階段有兩個主要功能:

  • 執行I/O回調。
  • 處理輪詢隊列中的事件。

當事件循環進入poll階段而且在timers中沒有能夠執行定時器時,將發生如下兩種狀況之一

  • 若是poll隊列不爲空,則事件循環將遍歷其同步執行它們的callback隊列,直到隊列爲空,或者達到system-dependent(系統相關限制)。

若是poll隊列爲空,則會發生如下兩種狀況之一

  • 若是有setImmediate()回調須要執行,則會當即中止執行poll階段並進入執行check階段以執行回調。

  • 若是沒有setImmediate()回到須要執行,poll階段將等待callback被添加到隊列中,而後當即執行。

固然設定了 timer 的話且 poll 隊列爲空,則會判斷是否有 timer 超時,若是有的話會回到 timer 階段執行回調。

check

此階段容許人員在poll階段完成後當即執行回調。
若是poll階段閒置而且script已排隊setImmediate(),則事件循環到達check階段執行而不是繼續等待。

setImmediate()其實是一個特殊的計時器,它在事件循環的一個單獨階段運行。它使用libuv API來調度在poll階段完成後執行的回調。

一般,當代碼被執行時,事件循環最終將達到poll階段,它將等待傳入鏈接,請求等。
可是,若是已經調度了回調setImmediate(),而且輪詢階段變爲空閒,則它將結束而且到達check階段,而不是等待poll事件。

console.log('start') setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(() => { console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) Promise.resolve().then(function() { console.log('promise3') }) console.log('end') 複製代碼

若是node版本爲v11.x, 其結果與瀏覽器一致。

start
end
promise3
timer1
promise1
timer2
promise2

複製代碼

具體詳情能夠查看《又被node的eventloop坑了,此次是node的鍋》。

若是v10版本上述結果存在兩種狀況:

  • 若是time2定時器已經在執行隊列中了
start
end
promise3
timer1
timer2
promise1
promise2
複製代碼
  • 若是time2定時器沒有在執行對列中,執行結果爲
start
end
promise3
timer1
promise1
timer2
promise2
複製代碼

具體狀況能夠參考poll階段的兩種狀況。

從下圖可能更好理解:

 

 

setImmediate() 的setTimeout()的區別

setImmediatesetTimeout()是類似的,但根據它們被調用的時間以不一樣的方式表現。

  • setImmediate()設計用於在當前poll階段完成後check階段執行腳本 。
  • setTimeout() 安排在通過最小(ms)後運行的腳本,在timers階段執行。

舉個例子

setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); 複製代碼

執行定時器的順序將根據調用它們的上下文而有所不一樣。 若是從主模塊中調用二者,那麼時間將受到進程性能的限制。

其結果也不一致

若是在I / O週期內移動兩個調用,則始終首先執行當即回調:

const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); }); 複製代碼

其結果能夠肯定必定是immediate => timeout
主要緣由是在I/O階段讀取文件後,事件循環會先進入poll階段,發現有setImmediate須要執行,會當即進入check階段執行setImmediate的回調。

而後再進入timers階段,執行setTimeout,打印timeout

┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
複製代碼

Process.nextTick()

process.nextTick()雖然它是異步API的一部分,但未在圖中顯示。這是由於process.nextTick()從技術上講,它不是事件循環的一部分。

  • process.nextTick()方法將 callback 添加到next tick隊列。 一旦當前事件輪詢隊列的任務所有完成,在next tick隊列中的全部callbacks會被依次調用。

換種理解方式:

  • 當每一個階段完成後,若是存在 nextTick 隊列,就會清空隊列中的全部回調函數,而且優先於其餘 microtask 執行。

例子

let bar; setTimeout(() => { console.log('setTimeout'); }, 0) setImmediate(() => { console.log('setImmediate'); }) function someAsyncApiCall(callback) { process.nextTick(callback); } someAsyncApiCall(() => { console.log('bar', bar); // 1 }); bar = 1; 複製代碼

在NodeV10中上述代碼執行可能有兩種答案,一種爲:

bar 1
setTimeout setImmediate 複製代碼

另外一種爲:

bar 1
setImmediate setTimeout 複製代碼

不管哪一種,始終都是先執行process.nextTick(callback),打印bar 1

最後

感謝@Dante_Hu提出這個問題await的問題,文章已經修正。 修改了node端執行結果。V10和V11的區別。

關於await問題參考瞭如下文章:.

promise, async, await, execution order
Normative: Reduce the number of ticks in async/await
async/await 在chrome 環境和 node 環境的 執行結果不一致,求解?
更快的異步函數和 Promise

其餘內容參考了:

JS瀏覽器事件循環機制
什麼是瀏覽器的事件循環(Event Loop)?
一篇文章教會你Event loop——瀏覽器和Node
不要混淆nodejs和瀏覽器中的event loop
瀏覽器與Node的事件循環(Event Loop)有何區別?
Tasks, microtasks, queues and schedules
前端面試之道
Node.js介紹5-libuv的基本概念
The Node.js Event Loop, Timers, and process.nextTick()
node官網

相關文章
相關標籤/搜索