簡單來說:html
對象放在heap(堆)裏,常見的基礎類型和函數放在stack(棧)裏,函數執行的時候在棧裏執行。棧裏函數執行的時候可能會調一些Dom操做,ajax操做和setTimeout定時器,這時候要等stack(棧)裏面的全部程序先走(注意:棧裏的代碼是先進後出),走完後再走WebAPIs,WebAPIs執行後的結果放在callback queue(回調的隊列裏,注意:隊列裏的代碼先放進去的先執行),也就是當棧裏面的程序走完以後,再從任務隊列中讀取事件,將隊列中的事件放到執行棧中依次執行,這個過程是循環不斷的。vue
簡單來說:html5
整個的這種運行機制又稱爲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事件環以前,咱們先了解一下宏任務和微任務在瀏覽器中的執行機制。也是面試中常常會被問到的。
任務可分爲宏任務和微任務,宏任務和微任務都是隊列
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
複製代碼
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()
其兩者的調用順序取決於當前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
複製代碼