JavaScript運行機制:event-loop

1、前言

JavaScript語言的特色是單線程,單線程只是指主線程,但不論是瀏覽器執行環境仍是node執行環境,除了主線程還有其餘的線程,如:網絡線程,定時器觸發線程,事件觸發線程等等,這些線程是如何與主線程協同工做的呢?node

2、任務隊列

這裏不得不提一個任務隊列的概念,js代碼中全部代碼分兩種:同步任務、異步任務。ajax

  • 全部同步任務都在主線程上執行,造成一個執行棧;數據庫

  • 主線程以外,還存在一個任務隊列,只要異步任務有了運行結果,就在任務隊列中放置一個事件;api

  • 一旦執行棧中全部同步任務執行完畢,系統就會讀取任務隊列,那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。數組

  • 主線程不斷重複上一步。promise

3、宏任務和微任務

瀏覽器和node中宏任務和微任務是不一樣的,後面詳細說明。下面先來了解宏任務和微任務的概念,宏任務和微任務都是任務隊列裏面的,能夠想象成任務隊列中其實有兩列,宏任務是一列,微任務是一列。瀏覽器

一、宏過任務

首先咱們把任務隊列裏面的任務稱爲task,瀏覽器爲了可以使得JS內部task與DOM任務可以有序的執行,會在一個task執行結束後,在下一個 task 執行開始前,對頁面進行從新渲染 (task->渲染->task->...),宏任務就是上述的 任務隊列裏的任務,嚴格按照時間順序壓棧和執行。如 setTimeOut、setInverter等,下圖爲瀏覽器與node中的宏任務。bash

二、微任務

微任務一般來講就是須要在當前 task 執行結束後當即執行的任務,好比對一系列動做作出反饋,或或者是須要異步的執行任務而又不須要分配一個新的 task,這樣即可以減少一點性能的開銷。只要執行棧中沒有其餘的js代碼正在執行且每一個宏任務執行完,微任務隊列會當即執行。若是在微任務執行期間微任務隊列加入了新的微任務,會將新的微任務加入隊列尾部,以後也會被執行。下圖爲瀏覽器與node中的微任務。網絡

4、事件環(Event Loop)

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

5、瀏覽器的事件環及對宏任務微任務的執行機制

主線程運行的時候,產生堆和棧,heap就是堆,堆裏面是存的是各類對象和函數,stack是棧,var a=1就存儲在棧內;dom事件,ajax請求,定時器等異步操做的回調會被放到任務隊列callback queue中,這個隊列時先進先出的順序,主線程執行完畢以後會依次執行callback queue中的任務,對應的異步任務就會結束等待狀態,進入主線程被執行。

一、瀏覽器的宏任務和微任務

當stack執行棧空的時候,當即執行microtask checkpoint ,microtask checkpoint 會檢查整個微任務隊列。因此就會執行微任務隊列中全部的任務,纔會去執行第一個宏任務,執行完第一個宏任務後,又會去清空微任務隊列。

具體支持分類以下: macro-task: setTimeout, setInterval, setImmediate, I/O, UI rendering,mesageChannel micro-task: Promises(這裏指瀏覽器實現的原生 Promise),Object.observe, MutationObserver

咱們用下面一段代碼來檢驗一下是否理解瀏覽器事件環:

setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(()=>{
        console.log('then1');

    })
},0)

Promise.resolve().then(()=>{
    console.log('then2');
    Promise.resolve().then(()=>{
        console.log('then3');
    })
    setTimeout(function(){
        console.log('setTimeout2')
    },0)
})
複製代碼

執行結果是then2 then3 setTimeout1 then1 setTimeout2

首先代碼裏面的setTimeout和Promise都是異步任務,js從上到下執行代碼,分別將這兩個異步任務放到了宏任務隊列和微任務隊列,執行棧此時爲空先清空微任務隊列,因此先輸出了then2,而後在微任務隊列中有添加一個then3的promise任務,在宏任務中添加了一個setTimeout2的定時器任務,因此接着執行下一個微任務,因此輸出了then3,開始執行第一個宏任務,輸出setTimeout1,而且在微任務隊列又添加then1的promise任務,因此轉去執行微任務,輸出then1,再去執行一個宏任務,就是以前放進去的setTimeout2.

6、node Event Loop

Node.js也是單線程的Event Loop,可是它的運行機制不一樣於瀏覽器環境。

node的代碼雖然也是運行在V8引擎上的,可是他還有一個libuv庫,專門處理異步i/o操做的,libuv庫底層是靠多線程加阻塞I/O模擬實現的異步i/o實現的。 根據上圖,Node.js的運行機制以下:

  • 一、咱們寫的js代碼會交給v8引擎進行解析;
  • 二、代碼中可能會調用node api,node會交給libuv庫處理
  • 三、libuv經過阻塞i/o和多線程實現了異步i/o
  • 四、經過事件驅動的方式,將結果放到事件隊列中,最終交給咱們的應用。

Node在進程啓動時,便會建立一個相似於while(true)的循環,每執行一次循環體的過程被稱爲tick,中文翻譯應該意爲「滴答」,就像時鐘同樣,每滴答一下,就表示過去了1s。這個tick也有點這個意思,每循環一次,都表示本次tick結束,下次tick開始。每一個tick開始之初,都會檢查是否有事件須要處理,若是有,就取出事件及關聯的callbak函數,若是存在有關聯的callback函數,就把事件的結果做爲參數調用這個callback函數執行。若是不在有事件處理,就退出進程。

那麼在每一個tick的過程當中,如何判斷是否有事件須要處理,先要引入一個概念,叫作「觀察者」(watcher)。每個事件循環都有一個或者多個觀察者,判斷是否有事件要處理的過程就是向這些觀察者詢問是否有須要處理的事件

Node的觀察者有這樣幾種:

  • 定時器觀察者:setTimeout,setInterval

  • idle觀察者:顧名思義,就是早已等在那裏的觀察者,之後會說到的process.nextTick就屬於這類

  • I/O觀察者:顧名思義,就是I/O相關觀察者,也就是I/O的回調事件,如網絡,文件,數據庫I/O等

  • check觀察者:顧名思義,就是須要檢查的觀察者,後面會說到的setImmediate就屬於這類

事件循環是一個典型的生產者/消費者模型。異步I/O,網絡請求,setTimeout等都是典型的事件生產者,源源不斷的爲Node提供不一樣類型的事件,這些事件被傳到對應的觀察者那裏,事件循環在每次tick時則從觀察者那裏取出事件並處理。

咱們如今知道,JavaScript的異步I/O調用過程當中,回調函數並不禁咱們開發者調用,事實上,在JavaScript發起調用到內核執行完I/O操做的過程當中,存在一種中間產物,它叫作請求對象。這個請求對象會從新封裝回調函數及參數,並作一些其餘的處理。這個請求對象,會在異步事件完成時被調用,取出回調函數和參數,並傳入執行結果進行回調。

組裝好請求對象,送入I/O線程池等待執行,實際上只是完成了異步I/O的第一步;第二步則是異步I/O被線程池處理結束後的回調,也就是執行回調。

應該說,事件循環、觀察者、請求對象、I/O線程池,這四者共同組成了Node異步I/O模型的基本要素。

不一樣類型的觀察者,處理的優先級不一樣,idle觀察者最早,I/O觀察者其次,check觀察者最後。

setTimeout()和setInterval()分別用於單次和屢次運行任務,其建立的定時器會被插入到定時器觀察者內部的一個紅黑樹中。每次Tick運行時,會從該紅黑樹中迭代取出定時器對象,檢查是否超過定時時間,若超過則造成一個事件,其回調函數立刻運行。

一、Node中宏任務和微任務

執行機制:

一、初始化Event loop;

二、執行主代碼,遇到異步處理,就分配給對應的隊列,直到主代碼執行完畢;

三、主代碼中遇到全部的微任務,先去執行全部的nextTick(),而後執行其餘的微任務,就是nextTick()在微任務裏面等級最高;

四、開始Event loop,就是上面的各個觀察者按順序檢查;

五、每次執行完畢一個觀察者隊列,轉下一個觀察者以前,會清空微任務隊列;

六、timer階段的定時器是不許的,在超過規定時間後,一旦獲得執行機會就當即執行。

promise的then是微任務,process.nextTick()也是微任務,執行順序是nextTick大於then

Promise.resolve().then(()=>{
    console.log('then');
})
process.nextTick(()=>{
    console.log('nextTick');
})
複製代碼

上面代碼先輸出nextTick,後輸出then 咱們能夠利用process.nextTick是異步任務,而且執行快的特色實現一些巧妙的解決辦法。

class A{
    constructor(){
        this.arr=[];
        process.nextTick(()=>{   
            console.log(this.arr);
        })
    }
    add(val){
        this.arr.push(val);
    }
}
let a=new A();
a.add('123');
a.add('456');
複製代碼

假如咱們這裏沒有加process.nextTick的時候,這裏打印出來的空數組,由於new實例的時候,就執行了constructor了,可是加了這個process.nextTick後,裏面的代碼會等同步代碼先執行完畢後再執行,這是就已經拿到了數據。打印出['123','456']。

setTimeout(()=>{
    console.log('timeout1');
    process.nextTick(()=>{
        console.log('nextTick');
    })
},1000)
setTimeout(()=>{
    console.log('timeout2')
},1000)

複製代碼

輸出:timeout1 timeout2 nextTick 先清空時間隊列,去執行下一個隊列以前,先去清空微任務隊列,也就是idle隊列,因此順序是這樣的

setTimeout(()=>{
    console.log('timeout1');
    process.nextTick(()=>{
        console.log('nextTick1');
    })
},1000)
process.nextTick(()=>{
    setTimeout(()=>{
        console.log('timeout2')
    },1000)
    console.log('nextTick2');
})
複製代碼

上面代碼的執行順序是不固定的,有時候

nextTick2 timeout1 nextTick1 timeout2

nextTick2 timeout1 timeout2 nextTick1

timer階段的定時器是不許的,他是在超過規定時間後,一旦獲得執行機會就當即執行。

上面代碼,先走idle隊列,先輸出nextTick2是固定的,這時候定時器隊列中放了兩個定時器了。確定是限制性timeout1,由於他是先放進去的,可是第一個定時器執行完畢後,第二個定時器不必定到結束時間,因此就會去執行idle隊列,輸出nextTick1,以後再執行timeout2。

第一個定時器是1000毫秒,可是第二個定時器的結束時間多是1000.8ms,由於process。nextTick也須要執行時間。第一個定時器執行完以後,可能還沒到1000.8ms,因此他就去清空了idle任務隊列,若是第一個定時器執行完畢後,已經到了1000.8ms,那麼確定先執行第二個定時器。

因此定時器的時間在底層實現的時候是不同的。

又一個例子

setImmediate(()=>{
    console.log('setImmediate');
})
setTimeout(()=>{
    console.log('setTimeout');
},0);  //規範是4ms,這裏規定的時間0,在底層實現的時候不是0ms
複製代碼

輸出:誰均可能先輸出

咱們知道setImmediate是check檢查隊列中的,node執行棧執行時間若是是5ms,那麼走到時間隊列的時候,定時器時間就已經到了,因此先執行setTimeout,再執行setImmediate,可是也有可能node執行棧中代碼執行了2ms,沒到4ms,就會先走setImmediate,再走時間隊列。

let fs=require('fs');
fs.readFile('./1.txt',function(){
    setImmediate(()=>{
        console.log('setImmediate');
    })
    setTimeout(()=>{
        console.log('setTimeout');
    },0);
})
複製代碼

文件讀取會走poll輪詢階段,獲得回調信息後,下一階段是check階段,因此setImmediate永遠先走。執行結果順序永遠同樣

最後一個小測試

let fs=require('fs');
setImmediate(()=>{
    Promise.resolve().then(()=>{
        console.log('then1');
    })
},0)
Promise.resolve().then(()=>{
    console.log('then2');
})
fs.readFile('./1.txt',function(){
    process.nextTick(()=>{
        console.log('nextTick');
    })
    setImmediate(()=>{
        console.log('setImmediate');
    })
})
複製代碼

答案在下面哦~

then2 then1 nextTick setImmediate

第一次確定是執行微任務輸出then2,而後走poll階段文件讀取,文件讀取不是馬上執行回調函數的,由於異步任務須要時間等待讀取結果,執行棧也不是在等着他執行完畢的,直接執行check階段,執行setImmediate的回調函數,裏面遇到了微任務,如今微任務隊列被添加進去一個,在執行fs的回調以前,清空微任務隊列,因此輸出then1,接着執行fs的回調,添加進去nextTick微任務,check階段的setImmediate,走完poll階段,確定要去清空微任務隊列,輸出nextTick,再走check階段,輸出setImmediate。

相關文章
相關標籤/搜索