瀏覽器說:雖然都叫event loop,可是我和node不同

討論event loop要作到如下兩點

  • 首先要肯定好上下文,nodejs和瀏覽器的event loop是兩個有明確區分的事物,不能混爲一談。
  • 其次,討論一些js異步代碼的執行順序時候,要基於node的源碼而不是本身的臆想。

簡單來說:html

  • nodejs的event是基於libuv,而瀏覽器的event loop則在html5的規範中明肯定義。
  • libuv已經對event loop做出了實現,而html5規範中只是定義了瀏覽器中event loop的模型,具體實現留給了瀏覽器廠商。

瀏覽器中的event loop

瀏覽器事件環中js分爲兩部分,一個叫heap(堆),一個叫stack(棧)。

對象放在heap(堆)裏,常見的基礎類型和函數放在stack(棧)裏,函數執行的時候在棧裏執行。棧裏函數執行的時候可能會調一些Dom操做,ajax操做和setTimeout定時器,這時候要等stack(棧)裏面的全部程序先走(注意:棧裏的代碼是先進後出),走完後再走WebAPIs,WebAPIs執行後的結果放在callback queue(回調的隊列裏,注意:隊列裏的代碼先放進去的先執行),也就是當棧裏面的程序走完以後,再從任務隊列中讀取事件,將隊列中的事件放到執行棧中依次執行,這個過程是循環不斷的。vue

簡單來說:html5

  • 1.全部同步任務都在主線程上執行,造成一個執行棧
  • 2.主線程以外,還存在一個任務隊列。只要異步任務有了運行結果,就在任務隊列之中放置一個事件。
  • 3.一旦執行棧中的全部同步任務執行完畢,系統就會讀取任務隊列,將隊列中的事件放到執行棧中依次執行
  • 4.主線程從任務隊列中讀取事件,這個過程是循環不斷的

整個的這種運行機制又稱爲Event Loop(事件循環)node

概念中首先要明白是:stack(棧)和queue(隊列)的區別,它們是怎麼去執行的?面試

棧方法LIFO(Last In First Out):先進後出(先進的後出),典型的就是函數調用。ajax

//執行上下文棧  做用域
var a = "aa";
function one(){
    let a = 1;
    two();
    function two(){
        let b = 2;
        three();
        function three(){
            console.log(b)
        }
    }
}
console.log(a);
one();

複製代碼
aa
2
複製代碼

圖解執行原理:promise

執行棧裏面最早放的是全局做用域(代碼執行有一個全局文本的環境),而後再放one, one執行再把two放進來,two執行再把three放進來,一層疊一層。

那麼怎麼出呢,怎麼銷燬的呢?瀏覽器

最早走的確定是three,由於two要是先銷燬了,那three的代碼b就拿不到了,因此是先進後出(先進的後出),因此,three最早出,而後是two出,再是one出。bash

隊列方法FIFO(First In First Out)多線程

(隊頭)[1,2,3,4](隊尾) 進的時候從隊尾依次進1,2,3,4 出的時候從對頭依次出1,2,3,4

瀏覽器事件環中代碼執行都是按棧的結果去執行的,可是咱們調用完多線程的方法(WebAPIs),這些多線程的方法是放在隊列裏的,也就是先放到隊列裏的方法先執行。

那何時WebAPIs裏的方法會再執行呢?

好比:stack(棧)裏面都走完以後,就會依次讀取任務隊列,將隊列中的事件放到執行棧中依次執行,這個時候棧中又出現了事件,這個事件又去調用了WebAPIs裏的異步方法,那這些異步方法會在再被調用的時候放在隊列裏,而後這個主線程(也就是stack)執行完後又將從任務隊列中依次讀取事件,這個過程是循環不斷的。

下面經過列子來講明:

例子1

console.log(1);
console.log(2);
setTimeout(function(){
    console.log(3);
})
setTimeout(function(){
    console.log(4);
})
console.log(5);
複製代碼
// 結果
1
2
5
3
4
複製代碼

一、首先執行棧裏面的同步代碼

1

2

5

二、棧裏面的setTimeout事件會依次放到任務隊列中,當棧裏面都執行完以後,再依次從從任務隊列中讀取事件往棧裏面去執行。

3

4

例子2

console.log(1);
console.log(2);

setTimeout(function(){
    console.log(3);
    setTimeout(function(){
        console.log(6);
    })
})
setTimeout(function(){
    console.log(4);
    setTimeout(function(){
        console.log(7);
    })
})
console.log(5)
複製代碼
// 結果
1
2
5
3
4
6
7
複製代碼

一、首先執行棧裏面的同步代碼

1

2

5

二、棧裏面的setTimeout事件會依次放到任務隊列中,當棧裏面都執行完以後,再依次從從任務隊列中讀取事件往棧裏面去執行。

3

4

三、當執行棧開始依次執行setTimeout時,會將setTimeout裏面的嵌套setTimeout依次放入隊列中,而後當執行棧中的setTimeout執行完畢後,再依次從從任務隊列中讀取事件往棧裏面去執行。

6

7

例子3

console.log(1);
console.log(2);

setTimeout(function(){
    console.log(3);
    setTimeout(function(){
        console.log(6);
    })
},400)
setTimeout(function(){
    console.log(4);
    setTimeout(function(){
        console.log(7);
    })
},100)
console.log(5)
複製代碼
// 結果
1
2
5
4
7
3
6
複製代碼

在例子2的基礎上,若是設置了setTimeout的時間,那就是按setTimeout的成功時間依次執行。

如上:這裏的順序是1,2,5,4,7,3,6。也就是隻要兩個set時間不同的時候 ,就set時間短的先走完,包括set裏面的回調函數,再走set時間慢的。(由於只有當時間到了的時候,纔會把set放到隊列裏面去,這一點跟nodejs中的set設置了時間的機制差很少,能夠看nodejs中的例子6,也是會先走完時間短,再走時間慢的。)

例子4

當觸發回調函數時,會將回調函數放到隊列中。永遠都是棧裏面執行完後再從任務隊列中讀取事件往棧裏面去執行。

setTimeout(function(){
    console.log('setTimeout')
},4)
for(var i = 0;i<10;i++){
    console.log(i)
}
複製代碼
// 結果
0
1
2
3
4
5
6
7
8
9
setTimeout
複製代碼

在學習nodejs事件環以前,咱們先了解一下宏任務和微任務在瀏覽器中的執行機制。也是面試中常常會被問到的。

宏任務和微任務

任務可分爲宏任務和微任務,宏任務和微任務都是隊列

  • macro-task(宏任務): setTimeout, setInterval, setImmediate, I/O
  • micro-task(微任務): process.nextTick, 原生Promise(有些實現的promise將then方法放到了宏任務中),Object.observe(已廢棄), MutationObserver不兼容的,MessageChannel(消息通道,相似worker)

Promise.then(源碼見到Promise就用setTimeout),then方法不該該放到宏任務中(源碼中寫setTimeout是無可奈何的),默認瀏覽器的實現這個then放到了微任務中。例如:

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

先走console.log(1),這裏的new Promise()是當即執行的,因此是同步的,因爲這個then在console.log(2)後面執行的,因此不是同步,是異步的。

那這跟宏任務和微任務有什麼關係?

咱們能夠加一個setTimeout(宏任務)對比一下:

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(100)
})
console.log(2)
複製代碼
1
3
2
100
setTimeout
複製代碼

結論:在瀏覽器事件環機制中,同步代碼先執行 執行是在棧中執行的,而後微任務會先執行,再執行宏任務

MutationObserver例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <!-- 當dom加載完畢後,來一句渲染完成 -->
    <script>
        console.log(1)
        let observe = new MutationObserver(function(){
            console.log('渲染完成')
        });
        <!--監控app的節點列表是否渲染完成-->
        observe.observe(app,{
            childList:true
        })
        for(var i = 0;i<100;i++){
            let p = document.createElement('p');
            document.getElementById('app').appendChild(p);
        }
        for(var i = 0;i<100;i++){
            let p = document.createElement('p');
            document.getElementById('app').appendChild(p);
        }
        console.log(2)
    </script>
</body>
</html>
複製代碼
// 結果
1
2
渲染完成
複製代碼

MessageChannel例子

vue中nextTick的實現原理就是經過這個方法實現的

console.log(1);
let channel = new MessageChannel();
let port1 = channel.port1;
let port2 = channel.port2;
port1.onmessage = function(e){
    console.log(e.data);
}
console.log(2);
port2.postMessage(100);
console.log(3)
複製代碼
// 瀏覽器中console結果 會等全部同步代碼執行完再執行,因此是微任務晚於同步的
1
2
3
100
複製代碼

nodejs中的event loop

node的特色:異步 非阻塞i/o node經過LIBUV這個庫本身實現的異步,默認的狀況下是沒有異步的方法的。

nodejs中的event loop有6個階段,這裏咱們重點關注poll階段(fs的i/o操做,對文件的操做,i/o裏面的回調函數都放在這個階段)

event loop的每一次循環都須要依次通過上述的階段。 每一個階段都有本身的callback隊列,每當進入某個階段,都會從所屬的隊列中取出callback來執行,當隊列爲空或者被執行callback的數量達到系統的最大數量時,進入下一階段。這六個階段都執行完畢稱爲一輪循環。

下面經過列子來講明:

例子1

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise')
    })
})
setTimeout(function(){
    console.log('setTimeout2')
})
複製代碼
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise
複製代碼

圖解執行原理:

一、首先執行完棧裏面的代碼

console.log(1);

console.log(2);

二、從棧進入到event loop的timers階段,因爲nodejs的event loop是每一個階段的callback執行完畢後纔會進入下一個階段,因此會打印出timers階段的兩個setTimeout的回調

setTimeout1

setTimeout2

三、因爲node event中微任務不在event loop的任何階段執行,而是在各個階段切換的中間執行,即從一個階段切換到下個階段前執行。因此當times階段的callback執行完畢,準備切換到下一個階段時,執行微任務(打印出Piromise),

Promise

若是例子1看懂了,如下例子2-例子6本身走一遍。須要注意的是例子6,當setTimeout設置了時間,優先按時間順序執行(瀏覽器事件環中例子3差很少)。例子7,例子8是重點。

例子2

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise')
    })
},1000)
setTimeout(function(){
    console.log('setTimeout2')
},1000)
複製代碼
-> node eventloop.js
1
2
setTimeout1
Promise
setTimeout2
複製代碼

例子3

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise')
    })
},2000)
setTimeout(function(){
    console.log('setTimeout2')
},1000)
複製代碼
-> node eventloop.js
1
2
setTimeout2
setTimeout1
Promise
複製代碼

例子4

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise1')
    })
})
setTimeout(function(){
    console.log('setTimeout2')
    Promise.resolve().then(function(){
        console.log('Promise2')
    })
})
複製代碼
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise1
Promise2
複製代碼

例子5

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise1')
    })
},1000)
setTimeout(function(){
    console.log('setTimeout2')
    Promise.resolve().then(function(){
        console.log('Promise2')
    })
},1000)
複製代碼
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise1
Promise2
複製代碼

例子6

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise1')
    })
},2000)
setTimeout(function(){
    console.log('setTimeout2')
    Promise.resolve().then(function(){
        console.log('Promise2')
    })
},1000)
複製代碼
-> node eventloop.js
1
2
setTimeout2
Promise2
setTimeout1
Promise1
複製代碼

例子7:setImmediate() vs setTimeout()

  • setImmediate 設計在poll階段完成時執行,即check階段;
  • setTimeout 設計在poll階段爲空閒時,且設定時間到達後執行;但其在timer階段執行

其兩者的調用順序取決於當前event loop的上下文,若是他們在異步i/o callback以外調用,其執行前後順序是不肯定的

setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});
複製代碼
-> node eventloop.js
timeout
immediate

-> node eventloop.js
immediate
timeout
複製代碼

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

這是由於fs.readFile callback執行完後,程序設定了timer 和 setImmediate,所以poll階段不會被阻塞進而進入check階段先執行setImmediate,後進入timer階段執行setTimeout

var fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})
複製代碼
$ node eventloop.js
immediate
timeout
複製代碼

例子8:process.nextTick()

process.nextTick()不在event loop的任何階段執行,而是在各個階段切換的中間執行,即從一個階段切換到下個階段前執行。

function Fn(){
    this.arrs;
    process.nextTick(()=>{
        this.arrs();
    })
}
Fn.prototype.then = function(){
    this.arrs = function(){console.log(1)}
}
let fn = new Fn();
fn.then();
複製代碼
-> node eventloop.js
   1
複製代碼

不加process.nextTick,new Fn()的時候,this.arrs是undefind,this.arrs()執行會報錯;

加了process.nextTick,new Fn()的時候,this.arrs()不會執行(由於process.nextTick是微任務,只有在各個階段切換的中間執行,因此它會等到同步代碼執行完以後纔會執行)這個時候同步代碼fn.then()執行=>this.arrs = function(){console.log(1)},this.arrs變成了一個函數,同步執行完後再去執行process.nextTick(()=>{this.arrs();})就不會報錯。

須要注意的是:nextTick千萬不要寫遞歸,能夠放一些比setTimeout優先執行的任務

// 死循環,會一直執行微任務,卡機
function nextTick(){
    process.nextTick(function(){
        nextTick();
    })
}
nextTick()
setTimeout(function(){

},499)
複製代碼

最後再來段代碼加深理解

var fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
    process.nextTick(()=>{
      console.log('nextTick3');
    })
  });
  process.nextTick(()=>{
    console.log('nextTick1');
  })
  process.nextTick(()=>{
    console.log('nextTick2');
  })
});
複製代碼
-> node eventloop.js
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout
複製代碼

一、從poll —> check階段,先執行process.nextTick,

nextTick1

nextTick2

二、而後進入check,setImmediate,

setImmediate

三、執行完setImmediate後,出check,進入close callback前,執行process.nextTick

nextTick3

四、最後進入timer執行setTimeout

setTimeout

結論:在nodejs事件環機制中,微任務是在各個階段切換的中間去執行的。

最後

  • 在瀏覽器的事件環機制中,咱們須要瞭解的是棧和隊列是怎麼去執行的。

    棧:先進後出;隊列:先進先出。

    全部代碼在棧中執行,棧中的DOM,ajax,setTimeout會依次進入到隊列中,當棧中代碼執行完畢後,有微任務先會將微任務依次從隊列中取出放到執行棧中執行,最後再依次將隊列中的事件放到執行棧中依次執行。

  • 在nodejs的事件環機制中,咱們須要瞭解的是node的執行機制是階段型的,微任務不屬於任何階段,而是在各個階段切換的中間執行。nodejs把事件環分紅了6階段,這裏須要注意的是,當執行棧裏的同步代碼執行完畢切換到node的event loop時也屬於階段切換,這時候也會先去清空微任務。

  • 微任務和宏任務

    macro-task(宏任務): setTimeout, setInterval, setImmediate, I/O

    micro-task(微任務): process.nextTick, 原生Promise(有些實現的promise將then方法放到了宏任務中),Object.observe(已廢棄), MutationObserver不兼容的

問題

若是在執行宏任務的過程當中又發現了回調中有微任務,會把這個微任務提早到全部宏任務以前,等到這個微任務完成後再繼續執行宏任務嗎?

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise1')
    })
})
setTimeout(function(){
    console.log(3)
    Promise.resolve(1).then(function(){
        console.log('promise2')
    })
})
setTimeout(function(){
    console.log(4)
    Promise.resolve(1).then(function(){
        console.log('promise3')
    })
})
複製代碼
// node中 每一個階段切換中間執行微任務
1
2
3
4
promise1
promise2
promise3
複製代碼
// 瀏覽器中  先走微任務
1
VM59:3 2
VM59:5 promise1
VM59:9 3
VM59:11 promise2
VM59:15 4
VM59:17 promise3
複製代碼

如下例子也能夠看看

// 例子1
console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise1')

        setTimeout(function(){
            console.log(3)
            Promise.resolve(1).then(function(){
                console.log('promise2')
            })
        })

    })
})
複製代碼
// node
1
2
promise1
3
promise2
複製代碼
// 瀏覽器
1
VM70:3 2
VM70:5 promise1
VM70:8 3
VM70:10 promise2
複製代碼
// 例子2
console.log(11);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise1')
    })
    setTimeout(function(){
        console.log(3)
        Promise.resolve(1).then(function(){
            console.log('promise2')
        })
    })
})
複製代碼
// node
11
2
promise1
3
promise2
複製代碼
// 瀏覽器
11
VM73:4 2
VM73:6 promise1
VM73:9 3
VM73:11 promise2
複製代碼
相關文章
相關標籤/搜索