討論event loop要做到以下兩點(diǎn)
- 首先要確定好上下文,nodejs和瀏覽器的event loop是兩個(gè)有明確區(qū)分的事物,不能混為一談。愛掏網(wǎng) - it200.com
- 其次,討論一些js異步代碼的執(zhí)行順序時(shí)候,要基于node的源碼而不是自己的臆想。愛掏網(wǎng) - it200.com
簡單來講:
- nodejs的event是基于libuv,而瀏覽器的event loop則在html5的規(guī)范中明確定義。愛掏網(wǎng) - it200.com
- libuv已經(jīng)對(duì)event loop作出了實(shí)現(xiàn),而html5規(guī)范中只是定義了瀏覽器中event loop的模型,具體實(shí)現(xiàn)留給了瀏覽器廠商。愛掏網(wǎng) - it200.com
瀏覽器中的event loop
對(duì)象放在heap(堆)里,常見的基礎(chǔ)類型和函數(shù)放在stack(棧)里,函數(shù)執(zhí)行的時(shí)候在棧里執(zhí)行。愛掏網(wǎng) - it200.com棧里函數(shù)執(zhí)行的時(shí)候可能會(huì)調(diào)一些Dom操作,ajax操作和setTimeout定時(shí)器,這時(shí)候要等stack(棧)里面的所有程序先走(注意:棧里的代碼是先進(jìn)后出),走完后再走WebAPIs,WebAPIs執(zhí)行后的結(jié)果放在callback queue(回調(diào)的隊(duì)列里,注意:隊(duì)列里的代碼先放進(jìn)去的先執(zhí)行),也就是當(dāng)棧里面的程序走完之后,再從任務(wù)隊(duì)列中讀取事件,將隊(duì)列中的事件放到執(zhí)行棧中依次執(zhí)行,這個(gè)過程是循環(huán)不斷的。愛掏網(wǎng) - it200.com
簡單來講:
- 1.所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧
- 2.主線程之外,還存在一個(gè)任務(wù)隊(duì)列。愛掏網(wǎng) - it200.com只要異步任務(wù)有了運(yùn)行結(jié)果,就在任務(wù)隊(duì)列之中放置一個(gè)事件。愛掏網(wǎng) - it200.com
- 3.一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列,將隊(duì)列中的事件放到執(zhí)行棧中依次執(zhí)行
- 4.主線程從任務(wù)隊(duì)列中讀取事件,這個(gè)過程是循環(huán)不斷的
整個(gè)的這種運(yùn)行機(jī)制又稱為Event Loop(事件循環(huán))
概念中首先要明白是:stack(棧)和queue(隊(duì)列)的區(qū)別,它們是怎么去執(zhí)行的?
棧方法LIFO(Last In First Out):先進(jìn)后出(先進(jìn)的后出),典型的就是函數(shù)調(diào)用。愛掏網(wǎng) - it200.com
//執(zhí)行上下文棧 作用域
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
圖解執(zhí)行原理:
那么怎么出呢,怎么銷毀的呢?
最先走的肯定是three,因?yàn)閠wo要是先銷毀了,那three的代碼b就拿不到了,所以是先進(jìn)后出(先進(jìn)的后出),所以,three最先出,然后是two出,再是one出。愛掏網(wǎng) - it200.com
隊(duì)列方法FIFO(First In First Out)
(隊(duì)頭)[1,2,3,4](隊(duì)尾) 進(jìn)的時(shí)候從隊(duì)尾依次進(jìn)1,2,3,4 出的時(shí)候從對(duì)頭依次出1,2,3,4

瀏覽器事件環(huán)中代碼執(zhí)行都是按棧的結(jié)果去執(zhí)行的,但是我們調(diào)用完多線程的方法(WebAPIs),這些多線程的方法是放在隊(duì)列里的,也就是先放到隊(duì)列里的方法先執(zhí)行。愛掏網(wǎng) - it200.com
那什么時(shí)候WebAPIs里的方法會(huì)再執(zhí)行呢?
比如:stack(棧)里面都走完之后,就會(huì)依次讀取任務(wù)隊(duì)列,將隊(duì)列中的事件放到執(zhí)行棧中依次執(zhí)行,這個(gè)時(shí)候棧中又出現(xiàn)了事件,這個(gè)事件又去調(diào)用了WebAPIs里的異步方法,那這些異步方法會(huì)在再被調(diào)用的時(shí)候放在隊(duì)列里,然后這個(gè)主線程(也就是stack)執(zhí)行完后又將從任務(wù)隊(duì)列中依次讀取事件,這個(gè)過程是循環(huán)不斷的。愛掏網(wǎng) - it200.com
下面通過列子來說明:
例子1
console.log(1);
console.log(2);
setTimeout(function(){
console.log(3);
})
setTimeout(function(){
console.log(4);
})
console.log(5);
// 結(jié)果
1
2
5
3
4
1、首先執(zhí)行棧里面的同步代碼 1
2
5
2、棧里面的setTimeout事件會(huì)依次放到任務(wù)隊(duì)列中,當(dāng)棧里面都執(zhí)行完之后,再依次從從任務(wù)隊(duì)列中讀取事件往棧里面去執(zhí)行。愛掏網(wǎng) - it200.com
3
4
例子2console.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)
// 結(jié)果
1
2
5
3
4
6
7
1、首先執(zhí)行棧里面的同步代碼
1
2
5
2、棧里面的setTimeout事件會(huì)依次放到任務(wù)隊(duì)列中,當(dāng)棧里面都執(zhí)行完之后,再依次從從任務(wù)隊(duì)列中讀取事件往棧里面去執(zhí)行。愛掏網(wǎng) - it200.com
3
4
3、當(dāng)執(zhí)行棧開始依次執(zhí)行setTimeout時(shí),會(huì)將setTimeout里面的嵌套setTimeout依次放入隊(duì)列中,然后當(dāng)執(zhí)行棧中的setTimeout執(zhí)行完畢后,再依次從從任務(wù)隊(duì)列中讀取事件往棧里面去執(zhí)行。愛掏網(wǎng) - it200.com
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)
// 結(jié)果
1
2
5
4
7
3
6
在例子2的基礎(chǔ)上,如果設(shè)置了setTimeout的時(shí)間,那就是按setTimeout的成功時(shí)間依次執(zhí)行。愛掏網(wǎng) - it200.com
如上:這里的順序是1,2,5,4,7,3,6。愛掏網(wǎng) - it200.com也就是只要兩個(gè)set時(shí)間不一樣的時(shí)候 ,就set時(shí)間短的先走完,包括set里面的回調(diào)函數(shù),再走set時(shí)間慢的。愛掏網(wǎng) - it200.com(因?yàn)橹挥挟?dāng)時(shí)間到了的時(shí)候,才會(huì)把set放到隊(duì)列里面去,這一點(diǎn)跟nodejs中的set設(shè)置了時(shí)間的機(jī)制差不多,可以看nodejs中的例子6,也是會(huì)先走完時(shí)間短,再走時(shí)間慢的。愛掏網(wǎng) - it200.com)
例子4
當(dāng)觸發(fā)回調(diào)函數(shù)時(shí),會(huì)將回調(diào)函數(shù)放到隊(duì)列中。愛掏網(wǎng) - it200.com永遠(yuǎn)都是棧里面執(zhí)行完后再從任務(wù)隊(duì)列中讀取事件往棧里面去執(zhí)行。愛掏網(wǎng) - it200.com
setTimeout(function(){
console.log('setTimeout')
},4)
for(var i = 0;i
// 結(jié)果
0
1
2
3
4
5
6
7
8
9
setTimeout
在學(xué)習(xí)nodejs事件環(huán)之前,我們先了解一下宏任務(wù)和微任務(wù)在瀏覽器中的執(zhí)行機(jī)制。愛掏網(wǎng) - it200.com也是面試中經(jīng)常會(huì)被問到的。愛掏網(wǎng) - it200.com
宏任務(wù)和微任務(wù)

任務(wù)可分為宏任務(wù)和微任務(wù),宏任務(wù)和微任務(wù)都是隊(duì)列
- macro-task(宏任務(wù)): setTimeout, setInterval, setImmediate, I/O
- micro-task(微任務(wù)): process.nextTick, 原生Promise(有些實(shí)現(xiàn)的promise將then方法放到了宏任務(wù)中),Object.observe(已廢棄), MutationObserver不兼容的,MessageChannel(消息通道,類似worker)
Promise.then(源碼見到Promise就用setTimeout),then方法不應(yīng)該放到宏任務(wù)中(源碼中寫setTimeout是迫不得已的),默認(rèn)瀏覽器的實(shí)現(xiàn)這個(gè)then放到了微任務(wù)中。愛掏網(wǎng) - it200.com例如:
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()是立即執(zhí)行的,所以是同步的,由于這個(gè)then在console.log(2)后面執(zhí)行的,所以不是同步,是異步的。愛掏網(wǎng) - it200.com
那這跟宏任務(wù)和微任務(wù)有什么關(guān)系?
我們可以加一個(gè)setTimeout(宏任務(wù))對(duì)比一下:
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
結(jié)論:在瀏覽器事件環(huán)機(jī)制中,同步代碼先執(zhí)行 執(zhí)行是在棧中執(zhí)行的,然后微任務(wù)會(huì)先執(zhí)行,再執(zhí)行宏任務(wù)
MutationObserver例子:
Document
// 結(jié)果
1
2
渲染完成
MessageChannel例子
vue中nextTick的實(shí)現(xiàn)原理就是通過這個(gè)方法實(shí)現(xiàn)的
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結(jié)果 會(huì)等所有同步代碼執(zhí)行完再執(zhí)行,所以是微任務(wù)晚于同步的
1
2
3
100
nodejs中的event loop
node的特點(diǎn):異步 非阻塞i/o node通過LIBUV這個(gè)庫自己實(shí)現(xiàn)的異步,默認(rèn)的情況下是沒有異步的方法的。愛掏網(wǎng) - it200.com
nodejs中的event loop有6個(gè)階段,這里我們重點(diǎn)關(guān)注poll階段(fs的i/o操作,對(duì)文件的操作,i/o里面的回調(diào)函數(shù)都放在這個(gè)階段)

event loop的每一次循環(huán)都需要依次經(jīng)過上述的階段。愛掏網(wǎng) - it200.com 每個(gè)階段都有自己的callback隊(duì)列,每當(dāng)進(jìn)入某個(gè)階段,都會(huì)從所屬的隊(duì)列中取出callback來執(zhí)行,當(dāng)隊(duì)列為空或者被執(zhí)行callback的數(shù)量達(dá)到系統(tǒng)的最大數(shù)量時(shí),進(jìn)入下一階段。愛掏網(wǎng) - it200.com這六個(gè)階段都執(zhí)行完畢稱為一輪循環(huán)。愛掏網(wǎng) - it200.com
下面通過列子來說明:
例子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
圖解執(zhí)行原理:

1、首先執(zhí)行完棧里面的代碼
console.log(1);
console.log(2);
2、從棧進(jìn)入到event loop的timers階段,由于nodejs的event loop是每個(gè)階段的callback執(zhí)行完畢后才會(huì)進(jìn)入下一個(gè)階段,所以會(huì)打印出timers階段的兩個(gè)setTimeout的回調(diào)
setTimeout1
setTimeout2
3、由于node event中微任務(wù)不在event loop的任何階段執(zhí)行,而是在各個(gè)階段切換的中間執(zhí)行,即從一個(gè)階段切換到下個(gè)階段前執(zhí)行。愛掏網(wǎng) - it200.com所以當(dāng)times階段的callback執(zhí)行完畢,準(zhǔn)備切換到下一個(gè)階段時(shí),執(zhí)行微任務(wù)(打印出Piromise),
Promise
如果例子1看懂了,以下例子2-例子6自己走一遍。愛掏網(wǎng) - it200.com需要注意的是例子6,當(dāng)setTimeout設(shè)置了時(shí)間,優(yōu)先按時(shí)間順序執(zhí)行(瀏覽器事件環(huán)中例子3差不多)。愛掏網(wǎng) - it200.com例子7,例子8是重點(diǎn)。愛掏網(wǎng) - it200.com
例子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
setTimeout2
Promise
例子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 設(shè)計(jì)在poll階段完成時(shí)執(zhí)行,即check階段;
- setTimeout 設(shè)計(jì)在poll階段為空閑時(shí),且設(shè)定時(shí)間到達(dá)后執(zhí)行;但其在timer階段執(zhí)行
其二者的調(diào)用順序取決于當(dāng)前event loop的上下文,如果他們?cè)诋惒絠/o callback之外調(diào)用,其執(zhí)行先后順序是不確定的
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
-> node eventloop.js
timeout
immediate
-> node eventloop.js
immediate
timeout
但當(dāng)二者在異步i/o callback內(nèi)部調(diào)用時(shí),總是先執(zhí)行setImmediate,再執(zhí)行setTimeout
這是因?yàn)閒s.readFile callback執(zhí)行完后,程序設(shè)定了timer 和 setImmediate,因此poll階段不會(huì)被阻塞進(jìn)而進(jìn)入check階段先執(zhí)行setImmediate,后進(jìn)入timer階段執(zhí)行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的任何階段執(zhí)行,而是在各個(gè)階段切換的中間執(zhí)行,即從一個(gè)階段切換到下個(gè)階段前執(zhí)行。愛掏網(wǎng) - it200.com
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()的時(shí)候,this.arrs是undefind,this.arrs()執(zhí)行會(huì)報(bào)錯(cuò);
加了process.nextTick,new Fn()的時(shí)候,this.arrs()不會(huì)執(zhí)行(因?yàn)閜rocess.nextTick是微任務(wù),只有在各個(gè)階段切換的中間執(zhí)行,所以它會(huì)等到同步代碼執(zhí)行完之后才會(huì)執(zhí)行)這個(gè)時(shí)候同步代碼fn.then()執(zhí)行=>this.arrs = function(){console.log(1)},this.arrs變成了一個(gè)函數(shù),同步執(zhí)行完后再去執(zhí)行process.nextTick(()=>{this.arrs();})就不會(huì)報(bào)錯(cuò)。愛掏網(wǎng) - it200.com
需要注意的是:nextTick千萬不要寫遞歸,可以放一些比setTimeout優(yōu)先執(zhí)行的任務(wù)
// 死循環(huán),會(huì)一直執(zhí)行微任務(wù),卡機(jī)
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階段,先執(zhí)行process.nextTick,
nextTick1
nextTick2
2、然后進(jìn)入check,setImmediate,
setImmediate
3、執(zhí)行完setImmediate后,出check,進(jìn)入close callback前,執(zhí)行process.nextTick
nextTick3
4、最后進(jìn)入timer執(zhí)行setTimeout
setTimeout
結(jié)論:在nodejs事件環(huán)機(jī)制中,微任務(wù)是在各個(gè)階段切換的中間去執(zhí)行的。愛掏網(wǎng) - it200.com
最后
-
在瀏覽器的事件環(huán)機(jī)制中,我們需要了解的是棧和隊(duì)列是怎么去執(zhí)行的。愛掏網(wǎng) - it200.com
棧:先進(jìn)后出;隊(duì)列:先進(jìn)先出。愛掏網(wǎng) - it200.com
所有代碼在棧中執(zhí)行,棧中的DOM,ajax,setTimeout會(huì)依次進(jìn)入到隊(duì)列中,當(dāng)棧中代碼執(zhí)行完畢后,有微任務(wù)先會(huì)將微任務(wù)依次從隊(duì)列中取出放到執(zhí)行棧中執(zhí)行,最后再依次將隊(duì)列中的事件放到執(zhí)行棧中依次執(zhí)行。愛掏網(wǎng) - it200.com
-
在nodejs的事件環(huán)機(jī)制中,我們需要了解的是node的執(zhí)行機(jī)制是階段型的,微任務(wù)不屬于任何階段,而是在各個(gè)階段切換的中間執(zhí)行。愛掏網(wǎng) - it200.comnodejs把事件環(huán)分成了6階段,這里需要注意的是,當(dāng)執(zhí)行棧里的同步代碼執(zhí)行完畢切換到node的event loop時(shí)也屬于階段切換,這時(shí)候也會(huì)先去清空微任務(wù)。愛掏網(wǎng) - it200.com
-
微任務(wù)和宏任務(wù)
macro-task(宏任務(wù)): setTimeout, setInterval, setImmediate, I/O
micro-task(微任務(wù)): process.nextTick, 原生Promise(有些實(shí)現(xiàn)的promise將then方法放到了宏任務(wù)中),Object.observe(已廢棄), MutationObserver不兼容的
問題
如果在執(zhí)行宏任務(wù)的過程中又發(fā)現(xiàn)了回調(diào)中有微任務(wù),會(huì)把這個(gè)微任務(wù)提前到所有宏任務(wù)之前,等到這個(gè)微任務(wù)完成后再繼續(xù)執(zhí)行宏任務(wù)嗎?
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中 每個(gè)階段切換中間執(zhí)行微任務(wù)
1
2
3
4
promise1
promise2
promise3
// 瀏覽器中 先走微任務(wù)
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
原文發(fā)布時(shí)間為:2024年06月04日
原文作者:我是家碧
本文來源:?掘金?如需轉(zhuǎn)載請(qǐng)聯(lián)系原作者