淺談event loop

Javascript引擎是單線程機制,首先咱們要了解Javascript語言爲何是單線程node

JavaScript的主要用途主要是用戶互動,和操做DOM。若是JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時這兩個節點會有很大沖突,爲了不這個衝突,因此決定了它只能是單線程,不然會帶來很複雜的同步問題。此外HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程(UI線程, 異步HTTP請求線程, 定時觸發器線程...),可是子線程徹底受主線程控制,這個新標準並無改變JavaScript單線程的本質。promise


在瞭解event loop以前,咱們先了解一下什麼是棧和隊列,他們有什麼特色?請先看兩張圖。瀏覽器

此處輸入圖片的描述
此處輸入圖片的描述
棧(stack) 是自動分配內存空間,它由系統自動釋放,特色是先進後出。 隊列的特色是先進先出。 再看一張圖:
此處輸入圖片的描述
咱們代碼執行的時候,都是在棧裏執行的,可是我調用多線程方法的時候是放到隊列裏的,先放進去的先執行。 那WebAPIs的方法何時放到棧裏執行呢? 當棧裏的代碼執行完了,會在隊列裏面讀取出來,放到棧執行。好比:寫個事件,事件裏面再調用異步方法,這些方法會在調用的時候,放到隊列裏,會不停的循環。等到隊列的代碼乾淨了,就中止循環了,否則就會一直循環。 看下面一串代碼,會輸出什麼?

console.log(1);
setTimeout(function(){
    console.log(2)
},0)
setTimeout(function(){
    console.log(3)
},0)
console.log('ok');
複製代碼

這段代碼中,會先把setTimeout的方法移到隊列中,當棧裏的代碼執行完以後,會把隊列裏方法取出來放到棧中執行,因此執行結果是:bash

1
ok
2
3
複製代碼

再對這串代碼進行擴展多線程

console.log(1);
//A
setTimeout(function(){
    console.log(2);
    //C
    setTimeout(function(){
        console.log(4);
        //D
        setTimeout(function(){
            console.log(5);
        })
    })
},0)
//B
setTimeout(function(){
    console.log(3);
    //E
    setTimeout(function(){
        console.log(6);
    })
},0)
console.log('ok');
複製代碼

這串代碼中,棧的代碼執行的時候,當觸發回調函數時,會將回調函數放到隊列中,因此,先輸出1和ok。棧裏的代碼執行完以後,會先讀取第一個setTimeout,輸出2,這時發現裏面還有一個setTimeout(既C行下的setTimeout),這個setTimeout又會放到隊列中去。而後執行B行下的setTimeout,輸出3,這時E行下還有個setTimeout,這個setTimeout又會放到隊列中。當棧裏代碼執行完以後,又會在隊列中讀取代碼,這時讀取的是C行下的setTimeout,放到棧執行,輸出4,緊接着又發現D行下有setTimeout,這個setTimeout又放到隊列中排隊。棧的代碼執行完了,又在隊列中讀取E行下的setTimeout,輸出6。執行完以後,又在隊列裏讀取D行下的setTimeout,輸出5。因此輸出結果是:異步

1
ok
2
3
4
6
5
複製代碼

附圖講解: 函數

此處輸入圖片的描述

setTiemout(function(){
    console.log(1)
},0)
for(var i = 0;i<1000;i++){
    console.log(i)
}
複製代碼

在當前隊列裏看到setTimeout,它會等着看事件何時成功。因此它會先往下走,走完之後,再把setTimeout裏的回調函數放到隊列中。即便for循環的代碼走了10s,回調函數也會等到10s後再執行。 因此,瀏覽器的機制永遠是:先走完棧裏代碼,纔會到隊列裏去。oop


宏任務和微任務

任務可分爲宏任務和微任務 宏任務:setTimeout,setInterval,setImmediate,I/O 微任務:process.nextTick,Promise.then 隊列能夠當作是一個宏任務。ui

微任務是怎麼執行的? 同步代碼先在棧中執行的,執行完以後,微任務會先執行,再執行宏任務。 先看一個例子:spa

console.log(1)
setTimeout(function(){
    console.log('setTimeout')
},0)
let promise = new Promise(function(resolve,reject){
    console.log(3);
    resolve(100);
}).then(function(data){
    console.log(200)
})
console.log(2)
複製代碼

想想會輸出什麼? 代碼由上到下執行,因此確定先輸出1。setTimeout是宏任務,會先放到隊列中。而new Promise是當即執行的,它是同步的,因此會先輸出3。由於then是異步的,因此會先輸出2。由於then是微任務,微任務走完,纔會走宏任務。因此最終輸出的結果是:1 3 2 200 setTimeout。 **注意:**瀏覽器的機制是把then方法放到微任務中。 瀏覽器機制:

此處輸入圖片的描述
代碼會先走咱們的執行棧,裏面有變量,函數等等。棧的代碼走完之後,會先去微任務,微任務裏面可能有不少回調函數(好比:棧裏有promise的then方法,then的回調函數會放到微任務裏去),棧裏面可能還有setTimeout,它會把setTimeout的回調函數放到宏任務中。何時放的呢?就是當時間到達的時候,會放到隊列裏。當棧的代碼都執行完了,它會先取微任務的then,執行。執行完以後,再取宏任務的代碼。(本身都快說暈了~~)

猜猜看:

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('ok')
    })
})
setTimeout(function(){
    console.log(3)
})
複製代碼

你猜輸出什麼~ 分析:先默認走棧,輸出1。此時並無微任務,因此微任務不會執行。先走第一個setTimeout,輸出2,同時將微任務放到隊列中,執行微任務,輸出ok,微任務執行完,再走宏任務,輸出3。

**注意:**瀏覽器和node環境輸出是不同的哦~

---------此處是分割線------------

node的event loop

接下來講說node的事件環。 先畫張圖吧

此處輸入圖片的描述
由圖能夠看出微任務不在事件環裏。那代碼怎麼走? 一樣上面的例題:

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('ok')
    })
})
setTimeout(function(){
    console.log(3)
})
複製代碼

先將2個定時器放到A中,先輸出1;這時候棧裏走完了,該走事件環了。在走事件環以前,會先將微任務清空,第一次微任務沒有東西,就濾過了。以後該走事件環了,這時候先走timers。這時候setTimeout不必定到達時間,若是到達時間,就直接執行了。若是時間沒到達,這時候可能先略過,接着往下走,走到poll輪詢階段,發現沒有讀文件之類的操做,而後它會等着,等到setTimeout的時間到達。若是時間到達了,它會把到達時間的定時器所有執行。好比先走第一個setTimeout,而且把then方法放到微任務中。它會把到達時間的setTimeout隊列所有清掉(所有執行完),再走微任務。假如poll輪詢有不少個I/O操做,它會把I/O操做都走完,再走timers。它是一個隊列一個隊列的清空,而不是取出一個,執行一下,取出一個,執行一下。因此它會把2個setTimeout都走完,再走then。因此在node的輸出結果是:

1
2
3
ok
複製代碼

再來個進階的栗子:

process.nextTick(function(){
    console.log(1)
})
setImmediate(function(){
    console.log(2)
})
複製代碼

它會先走棧的內容,棧啥都沒有。當它要走事件環的時候,會將微任務清空。發現微任務有nextTick,它會把nextTick執行完,再走事件環。發現timers和poll都沒有東西,它就會走theck階段。 nextTick 和 then都是在階段轉化時才調用。所謂的階段轉化,就是剛開始走當前棧,在當前棧轉到timers的時候,清空微任務。

事件循環的順序,決定js代碼的執行順序。進入總體代碼(宏任務)後,開始第一次循環。接着執行全部的微任務。而後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行全部的微任務。

Node.js的Event Loop

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

先說到這裏吧,有欠缺的後續再補充。

相關文章
相關標籤/搜索