瀏覽器和Node 中的Event Loop

前言

js與生俱來的就是單線程無阻塞的腳本語言。 做爲單線程語言,js代碼執行時都只有一個主線程執行任務。
無阻塞的實現依賴於咱們要談的事件循環。eventloop的規範是真的苦澀難懂,僅僅要理解的話,不推薦去硬啃。html

進程與線程

一直在說js是單線程語言。那麼什麼是線程呢,對於大部分前端同窗來講,可能並非那麼清晰。推薦阮大佬的這篇文章,形象生動
首先,計算機的核心是CPU,它承擔了全部的計算任務。它就像一座工廠,時刻在運行。前端

進程

進程就比如工廠的車間,它表明CPU所能處理的單個任務。
任一時刻,CPU老是運行一個進程,其餘進程處於非運行狀態。
即資源分配的最小單位,擁有獨立的堆棧空間和數據存儲空間html5

線程

線程就比如車間裏的工人。車間的空間是工人們共享的,這象徵一個進程的內存空間是共享的,每一個線程均可以使用這些共享內存。
即程序執行的最小單位,一個進程能夠包括多個線程。node

相對於進程來講,線程不涉及數據空間的操做,因此切換更高效,開銷小。web

js單線程的起源

顯然多進程能夠並行處理,提高cpu的利用率。
可是js初期是做爲腳本出現的,其要與DOM進行交互,以完成對用戶的展現。
若是多進程,同時操做DOM,那麼後果就不可控了。
例如:對於同一個按鈕,不一樣的進程賦予了不一樣的顏色,到底該怎麼展現。chrome

做爲一個腳本語言,若是使用多線程+鎖的話太多複雜了,因此js就是單線程了。api

不過隨着js的發展,承載的能力愈來愈多,侷限於單線程使得js的效率等有所限制。
所以增長了web worker來執行非dom的操做。瀏覽器

不過該線程非主線程有一些限制、例如不能操做DOM等,也就是爲了保證DOM操做的一致性,這裏就先不關注了。bash

咱們主要關注的仍是非阻塞的能力基礎,即事件循環。多線程

瀏覽器中的事件循環

說道事件循環就要先說事件隊列。 在主線程運行時,會產生堆(heap)和棧(stack)。

堆中存的是咱們聲明的object類型的數據,棧中存的是基本數據類型以及函數執行時的運行空間。

主線程從任務隊列中讀取事件,這個過程是循環不斷的,因此這種運行機制即Event Loop。

對於同步代碼,是直接執行的。 而執行異步方法時一樣會加入事件隊列中,可是異步事件是有差異的,差異在於執行的優先級不一樣。

事件分類

由於異步任務之間並不相同,所以他們的執行優先級也有區別。不一樣的異步任務被分爲兩類:微任務(micro task)和宏任務(macro task)。

  • 如下事件屬於宏任務:

setTimeout, setInterval, setImmediate,I/O, UI rendering

  • 如下事件屬於微任務

Promise,Object.observe(已廢棄),MutationObserver(html5新特性),process.nextTick

執行棧中的代碼(同步任務),老是在讀取"任務隊列"(異步任務)以前執行 當前執行棧執行完畢時會馬上先處理全部微任務隊列中的事件,而後再去宏任務隊列中取出一個事件。同一次事件循環中,微任務永遠在宏任務以前執行

對於不一樣類型的任務執行順序以下:

  1. 同步代碼執行
  2. event-loop start
  3. microTasks 隊列開始清空(執行)
  4. 檢查 Tasks 是否清空,有則跳到 4,無則跳到 6
  5. 從 Tasks 隊列抽取一個任務,執行
  6. 檢查 microTasks 是否清空,如有則跳到 2,無則跳到 3
  7. 結束 event-loop

大概流程圖以下:

不如直接看個栗子:

setTimeout(function () {
    console.log(1);
});

new Promise(function(resolve,reject){
    console.log(2)
    resolve(3)
}).then(function(val){
    console.log(val);
})
// 2 3 1 
複製代碼
  1. 區分事件類型:宏任務setTimeout,微任務.then
  2. 同步代碼執行 輸出2
  3. 微任務隊列清空 輸出 3
  4. 宏任務執行 輸出 1

下面來個稍微複雜的:

setTimeout(()=>{
    console.log('A');
},0);
var obj={
    func:function () {
        setTimeout(function () {
            console.log('B')
        },0);
        return new Promise(function (resolve) {
            console.log('C');
            resolve();
        })
    }
};
obj.func().then(function () {
    console.log('D')
});
console.log('E');
// c,e,d,b,a
複製代碼

你們能夠結合例子本身試下。

node中的事件循環機制

在node中,事件循環表現出的狀態與瀏覽器中大體相同。不一樣的是node中有一套本身的模型。
node中事件循環的實現是依靠的libuv引擎。
咱們知道node選擇chrome v8引擎做爲js解釋器,v8引擎將js代碼分析後去調用對應的node api,
而這些api最後則由libuv引擎驅動,執行對應的任務,並把不一樣的事件放在不一樣的隊列中等待主線程執行。
所以實際上node中的事件循環存在於libuv引擎中。

而node 事件分爲下面幾大階段:

  • timers: 這個階段執行setTimeout()和setInterval()設定的回調。
  • I/O callbacks: 執行幾乎全部的回調,除了close回調,timer的回調,和setImmediate()的回調。
  • idle, prepare: 僅內部使用。
  • poll: 獲取新的I/O事件;node會在適當條件下阻塞在這裏,等待新的I/O。
  • check: pool階段以後,執行setImmediate()設定的回調。
  • close callbacks: 執行好比socket.on('close', ...)的回調

poll階段

值得額外關注的是poll階段

該階段有以下功能:

  1. 執行 timer 階段到達時間上限的的任務。
  2. 執行 poll 階段的任務隊列。

若是進入 poll 階段,而且沒有 timer 階段加入的任務,將會發生如下狀況

  • 若是 poll 隊列不爲空的話,會執行 poll 隊列直到清空或者系統回調數達到上限
  • 若是 poll 隊列爲空 ​ 若是設定了 setImmediate 回調,會直接跳到 check 階段。 若是沒有設定 setImmediate 回調,會阻塞住進程,並等待新的 poll 任務加入並當即執行。

process.nextTick()

nextTick 比較特殊,它有本身的隊列,而且,獨立於event loop。 它的執行也很是特殊,不管 event loop 處於何種階段,都會在階段結束的時候清空 nextTick 隊列。

直接看例子吧: process.nextTick

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
複製代碼

大概順序以下:

  1. 由於nextTick的特殊性,當前階段執行完畢,就執行。因此直接,輸出1 2
  2. 執行到timer 輸出 TIMEOUT FIRED

setImmediate

setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);
複製代碼

這個結果不固定,同一臺機器測試結果也有兩種:

// TIMEOUT FIRED =>1 =>2
或者
//  1=>TIMEOUT FIRED=>2
複製代碼
  1. 事件隊列進入timer,性能好的 小於1ms,則不執行回調繼續往下。若此時大於1ms, 則輸出 TIMEOUT FIRED 就不輸出步驟3了。
  2. poll階段任務爲空,存在setImmediate 直接進入setImmediate 輸出1
  3. 而後再次到達timer 輸出 TIMEOUT FIRED
  4. 再次進入check 階段 輸出 2

緣由在於setTimeout 0 node 中至少爲1ms,也就是取決於機器執行至timer時是否到了可執行的時機。

作個對比就比較清楚了:

setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});

setImmediate(function B(){console.log(4);});
setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 20);
// 1=>2=>TIMEOUT FIRED
複製代碼

此時間隔時間較長,timer階段最後纔會執行,因此會先執行兩次check,出處1,2 下面再看個例子 poll階段任務隊列

var fs = require('fs')

fs.readFile('./yarn.lock', () => {
    setImmediate(() => {
        console.log('1')
        setImmediate(() => {
            console.log('2')
        })
    })
    setTimeout(() => {
        console.log('TIMEOUT FIRED')
    }, 0)
    
})
// 結果肯定:
// 輸出始終爲1=>TIMEOUT FIRED=>2
複製代碼
  1. 讀取文件,回調進入poll階段
  2. 當前無任務隊列,直接check 輸出1 將setImmediate2加入事件隊列
  3. 接着timer階段,輸出TIMEOUT FIRED
  4. 再次check階段,輸出2

小結

瀏覽器的事件循環 瀏覽器比較清晰一些,就是固定的流程,當前宏任務結束,就是執行全部微任務(不必定是所有,可能基於系統能力,會有所剩下),而後再下一個宏任務,微任務這樣交替進行。 node中的事件循環 主要是把握不一樣階段和特殊狀況的處理,特別是poll階段和 process.nextTick任務。

結束語

參考文章:

zhuanlan.zhihu.com/p/47152694 html.spec.whatwg.org/multipage/w… www.ruanyifeng.com/blog/2014/1… hackernoon.com/understandi… juejin.im/post/5bac87… 感謝上述參考文章,關於事件循環這裏就總結完畢了,做爲本身的一個學習心得。但願能幫助到有需求的同窗,一塊兒進步。

相關文章
相關標籤/搜索