讨論event loop要做到以下兩點
- 首先要确定好上下文,nodejs和浏覽器的event loop是兩個有明确區分的事物,不能混為一談。
- 其次,讨論一些js異步代碼的執行順序時候,要基于node的源碼而不是自己的臆想。
簡單來講:
- 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(回調的隊列裡,注意:隊列裡的代碼先放進去的先執行),也就是當棧裡面的程式走完之後,再從任務隊列中讀取事件,将隊列中的事件放到執行棧中依次執行,這個過程是循環不斷的。
- 1.所有同步任務都在主線程上執行,形成一個執行棧
- 2.主線程之外,還存在一個任務隊列。隻要異步任務有了運作結果,就在任務隊列之中放置一個事件。
- 3.一旦執行棧中的所有同步任務執行完畢,系統就會讀取任務隊列,将隊列中的事件放到執行棧中依次執行
- 4.主線程從任務隊列中讀取事件,這個過程是循環不斷的
整個的這種運作機制又稱為Event Loop(事件循環)
概念中首先要明白是:stack(棧)和queue(隊列)的差別,它們是怎麼去執行的?
棧方法LIFO(Last In First Out):先進後出(先進的後出),典型的就是函數調用。
//執行上下文棧 作用域
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
圖解執行原理:
執行棧裡面最先放的是全局作用域(代碼執行有一個全局文本的環境),然後再放one,
one執行再把two放進來,two執行再把three放進來,一層疊一層。
那麼怎麼出呢,怎麼銷毀的呢?
最先走的肯定是three,因為two要是先銷毀了,那three的代碼b就拿不到了,是以是先進後出(先進的後出),是以,three最先出,然後是two出,再是one出。
隊列方法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、首先執行棧裡面的同步代碼
1
2
5
2、棧裡面的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
3、當執行棧開始依次執行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的數量達到系統的最大數量時,進入下一階段。這六個階段都執行完畢稱為一輪循環。
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
1、首先執行完棧裡面的代碼
console.log(1);
console.log(2);
2、從棧進入到event loop的timers階段,由于nodejs的event loop是每個階段的callback執行完畢後才會進入下一個階段,是以會列印出timers階段的兩個setTimeout的回調
setTimeout1
setTimeout2
3、由于node event中微任務不在event loop的任何階段執行,而是在各個階段切換的中間執行,即從一個階段切換到下個階段前執行。是以當times階段的callback執行完畢,準備切換到下一個階段時,執行微任務(列印出Piromise),
Promise
如果例子1看懂了,以下例子2-例子6自己走一遍。需要注意的是例子6,當setTimeout設定了時間,優先按時間順序執行(浏覽器事件環中例子3差不多)。例子7,例子8是重點。
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
setTimeout2
Promise
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
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
1、從poll —> check階段,先執行process.nextTick,
nextTick1
nextTick2
2、然後進入check,setImmediate,
setImmediate
3、執行完setImmediate後,出check,進入close callback前,執行process.nextTick
nextTick3
4、最後進入timer執行setTimeout
setTimeout
結論:在nodejs事件環機制中,微任務是在各個階段切換的中間去執行的。
最後
-
在浏覽器的事件環機制中,我們需要了解的是棧和隊列是怎麼去執行的。
棧:先進後出;隊列:先進先出。
所有代碼在棧中執行,棧中的DOM,ajax,setTimeout會依次進入到隊列中,當棧中代碼執行完畢後,有微任務先會将微任務依次從隊列中取出放到執行棧中執行,最後再依次将隊列中的事件放到執行棧中依次執行。
- 在nodejs的事件環機制中,我們需要了解的是node的執行機制是階段型的,微任務不屬于任何階段,而是在各個階段切換的中間執行。nodejs把事件環分成了6階段,這裡需要注意的是,當執行棧裡的同步代碼執行完畢切換到node的event loop時也屬于階段切換,這時候也會先去清空微任務。
-
微任務和宏任務
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
原文釋出時間為:2018年06月04日
原文作者:我是家碧
本文來源:
掘金 https://juejin.im/entry/5b3a29f95188256228041f46如需轉載請聯系原作者