Javascript中的事件循環javascript
javascript是一門單線程的非阻塞的腳本語言。單線程,即js代碼在執行的任什麼時候候,都只有一個主線程來處理全部任務。非阻塞,只要指的是執行異步任務(如I/O事件)時,主線程會掛起這個任務,而後在異步任務返回結果的時候再按照必定規則執行相應的回調。java
Web worker 技術所實現的多線程技術也存在諸多限制。如,全部新線程都受到主線程的徹底控制,不能獨立執行。這意味着這些‘線程’其實是主線程的子線程。另外,這些子線程沒有執行I/O操做的權限,只能爲主線程分擔一些如計算等任務。因此嚴格來說,web worker並無改變javascript的單線程本質。node
執行棧與存儲對象指針和基礎類型變量的棧是不一樣的。執行棧是指,當調用一個方法時,js會生成與這個方法對應的一個執行環境(context),即執行上下文。這個執行環境中包含:這個執行環境的私有做用域、上層做用域的指向,方法的參數,私有變量以及該做用域的this指向。由於js是單線程的,同一時間只能執行一個方法,也就是說,當一個方法被執行的時候,其餘方法會被排隊到一個單獨的地方,即執行棧。web
當一個腳本第一次執行的時候,js引擎會解析這段代碼,並將其中的同步代碼按照執行順序加入執行棧,而後從頭開始執行。當執行一個方法時,js會向執行棧中添加這個方法的執行環境,而後進入這個執行環境繼續執行其中的代碼。當這個執行環境中的代碼執行完畢並返回結果後,js會退出當前執行環境並撤銷該環境,回到上一個方法的執行環境,這個過程反覆執行,知道執行棧中的代碼所有執行完畢。chrome
案列1:api
function Func1 () {瀏覽器
console.log(1)多線程
function Func2 () {異步
console.log(2)ui
function Func3 () {
console.log(3)
}
Func3()
}
Func2()
}
Func1()
// 1 2 3
同步執行遵循先進後出的規則,在執行Func1時,會向執行棧加入該方法的執行環境,輸出1,而後解析了Func2,執行時加入了Func2的執行環境,輸出2,而後解析Func3並執行,輸出3,Func3執行完畢後會撤銷Func3的執行環境,接着是Func2執行完畢並撤銷Func2的執行環境,最後撤銷Func1的執行環境。該過程若沒有終止,會無限進行直到棧溢出。
方法執行時,異步執行事件掛起加入與執行棧不一樣的另外一個隊列,即事件隊列中,並繼續執行執行棧中的其餘任務。被放入事件隊列不會當即執行其回調,而是等待當前執行棧中的全部任務執行完畢,在主線程出於閒置狀態時,主線程會查找事件隊列是否有任務。若是有,則會取第一個事件並將該事件的回調放入執行棧中執行,而後執行其中的同步代碼,如此反覆就是事件循環。
異步任務由於各任務的不一樣和執行優先級的區別,分爲 宏任務 (macro task) 和 微任務 (micro task)
屬於宏任務的事件:setTimeout(), setInterval()
屬於微任務的事件:new Promise(), new MutaionObserver()(已廢除)
當執行棧爲空時,主線程會優先查看微任務是否有事件。若是沒有,就會執行宏任務中的第一個事件並將對應的回調加入當前執行棧中;若是有,就會依次執行微任務中事件對應的回調,直到微任務隊列爲空,而後再執行宏任務中的第一個事件對應的回調,如此反覆,進入循環。同一次事件循環中,微任務永遠優先宏任務執行。
案列2:
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
// 2 3 1
node環境下的事件循環
在node中,事件循環與瀏覽器中的略有不一樣。node中的事件循環的實現是依靠的libuv引擎。node選用chrome的v8引擎做爲解釋器,v8引擎將js代碼解析後會調用node api,而api則是由libuv引擎驅動,所以node中的事件循環是在libuv引擎中執行。
node中,同步代碼執行完,會先清空微任務隊列,輪詢時會清空當前隊列全部任務,纔會切換到下一個隊列,在切換下一個隊列以前也會先清空微任務隊列。
(來自:node官網)
node的事件循環順序:
外部輸入數據—>poll階段—>檢查階段(check)—>關閉事件回調階段(close callback)—>定時器檢測執行階段(timers)—>I/O事件回調階段(I/O callbacks)—>idle,prepare—>poll…
setTimeout(() => {console.log('setTimeout')} , 0)
setImmediate(() => {console.log('immediate')})
默認狀況下setTimeout()和 setImmediate()不知道哪個會先執行,node執行也須要準備時間。setTimeout()延遲時間設置爲0,實際仍是有4ms的延遲,假設node準備時間在4ms內,定時器沒有執行,poll階段沒有執行setTimeout(),會先執行check中的setImmediate(),等到下一輪詢進入時,poll檢測到定時器已到時,再執行timer中的setTimeout()
隊列中有一個特殊的推遲任務執行的方法process.nextTick再此執行。咱們知道,每一次事件循環都是從微任務開始的,而且每一階段都是按照事件循環順序進行執行。而在每一次的隊列切換以前,都會檢查nextTick queue中是否有事件,如有則優先執行。
案列3:
setImmediate(() => {
console.log("setImmediate1");
setTimeout(() => {
console.log("setTimeout1");
}, 0);
});
setTimeout(() => {
process.nextTick(() => console.log("nextTick"));
console.log("setTimeout2");
setImmediate(() => {
console.log("setImmediate2");
});
}, 0);
// 結果一
// setImmediate1, setTimeout2, setTimeout1, nextTick, setImmediate2
// 結果二
// setTimeout2, nextTick, setImmediate1, setImmediate2, setTimeout1
產生上面兩種結果的緣由,是node準備時間的差別。
案例4:
const fs = require('fs');
fs.readFile(__filename, () => {
setImmediate(() => {
console.log("setImmediate1");
setTimeout(() => {
console.log("setTimeout1");
}, 0);
});
setTimeout(() => {
process.nextTick(() => console.log("nextTick"));
console.log("setTimeout2");
setImmediate(() => {
console.log("setImmediate2");
});
}, 0);
});
// setImmediate1, setTimeout2, setTimeout1, nextTick, setImmediate2
此時只會有一種結果,由於是在一個I/O事件的回調中,node準備已結束,setTimeout執行須要等待4ms,setImmediate則當即執行,又setTimeout2和setTimeout1在同一個timers隊列中因此按順序執行,以後須要切換到check隊列執行setImmediate2,在切換以前會先檢查nextTick隊列並執行,所以最後輸出nextTick,setImmediate2
注:歡迎你們監督指導,若有疑問或錯誤,請留言一塊兒探討~~