javascript是一門單線程的非阻塞的腳本語言。javascript
單線程:javascript代碼在執行的任什麼時候候,都只有一個主線程來處理全部的任務。java
單線程的緣由:瀏覽器中,咱們須要進行各類各樣的dom操做。若是javascript是多線程的,那麼當兩個線程同時對dom進行一項操做,例如一個向其添加事件,而另外一個刪除了這個dom,此時該如何處理呢?所以,爲了保證不會 發生相似情景,javascript選擇只用一個主線程來執行代碼,這樣就保證了程序執行的一致性。node
單線程在保證了執行順序的同時也限制了javascript的效率,所以開發出了web worker技術。這項技術號稱讓javascript成爲一門多線程語言。可是全部新線程都受主線程的徹底控制,不能獨立執行。這意味着這些「線程」 實際上應屬於主線程的子線程。這些子線程並無執行I/O操做的權限,只能爲主線程分擔一些諸如計算等任務。因此嚴格來說這些線程並無完整的功能,也所以這項技術並不是改變了javascript語言的單線程本質。web
當代碼須要進行一項異步任務(沒法馬上返回結果,須要花必定時間才能返回的任務,如I/O事件)的時候,主線程會掛起(pending)這個任務,而後在異步任務返回結果的時候再根據必定規則去執行相應的回調。chrome
javascript引擎實現非阻塞的關鍵就是——event loop(事件循環)api
當javascript代碼執行的時候會將不一樣的變量存於內存中的不一樣位置:堆(heap)和棧(stack)中來加以區分。其中,堆裏存放着一些對象。而棧中則存放着一些基礎類型變量以及對象的指針。promise
當咱們調用一個方法的時候,js會生成一個與這個方法對應的執行環境(context),又叫執行上下文。這個執行環境中存在着這個方法的私有做用域,上層做用域的指向,方法的參數,這個做用域中定義的變量以及這個做用域的this對象。瀏覽器
當一系列方法被依次調用的時候,由於js是單線程的,同一時間只能執行一個方法,因而這些方法被排隊在一個單獨的地方。這個地方被稱爲執行棧。markdown
當一個腳本第一次執行的時候,js引擎會解析這段代碼,並將其中的同步代碼按照執行順序加入執行棧中,而後從頭開始執行。若是當前執行的是一個方法,那麼js會向執行棧中添加這個方法的執行環境,而後進入這個執行環境繼續執行其中的代碼。當這個執行環境中的代碼 執行完畢並返回結果後,js會退出這個執行環境並把這個執行環境銷燬,回到上一個方法的執行環境。這個過程反覆進行,直到執行棧中的代碼所有執行完畢。多線程
一個方法執行會向執行棧中加入這個方法的執行環境,在這個執行環境中還能夠調用其餘方法,甚至是本身,其結果不過是在執行棧中再添加一個執行環境。這個過程能夠是無限進行下去的,除非發生了棧溢出,即超過了所能使用內存的最大值。
js引擎遇到一個異步事件後並不會一直等待其返回結果,而是會將這個事件掛起,繼續執行執行棧中的其餘任務。當一個異步事件返回結果後,js會將這個事件加入與當前執行棧不一樣的另外一個隊列,咱們稱之爲任務隊列。
被放入任務隊列不會馬上執行其回調,而是等待當前執行棧中的全部任務都執行完畢, 主線程處於閒置狀態時,主線程會去查找任務隊列是否有任務。若是有,那麼主線程會從中取出排在第一位的事件,並把這個事件對應的回調放入執行棧中,而後執行其中的同步代碼...,如此反覆,這樣就造成了一個無限的循環。這就是這個過程被稱爲「事件循環(Event Loop)」的緣由。
如下事件屬於宏任務:
如下事件屬於微任務
在當前執行棧爲空的時候,主線程會 查看微任務隊列是否有事件存在。若是不存在,那麼再去宏任務隊列中取出一個事件並把對應的回到加入當前執行棧;若是存在,則會依次執行隊列中事件對應的回調,直到微任務隊列爲空,而後去宏任務隊列中取出最前面的一個事件,把對應的回調加入當前執行棧...如此反覆,進入循環。
噹噹前執行棧執行完畢時會馬上先處理全部微任務隊列中的事件,而後再去宏任務隊列中取出一個事件。同一次事件循環中,微任務永遠在宏任務以前執行。
例子:
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
console.log(4);
//2
//4
//3
//1
複製代碼
在node中,事件循環表現出的狀態與瀏覽器中大體相同。不一樣的是node中有一套本身的模型。node中事件循環的實現是依靠的libuv引擎。node選擇chrome v8引擎做爲js解釋器,v8引擎將js代碼分析後去調用對應的node api,而這些api最後則由libuv引擎驅動,執行對應的任務,並把不一樣的事件放在不一樣的隊列中等待主線程執行。 所以實際上node中的事件循環存在於libuv引擎中。
一個libuv引擎中的事件循環的模型:
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
*注:模型中的每個方塊表明事件循環的一個階段*
複製代碼
這個模型是node官網上的一篇文章中給出的。
從上面這個模型中,咱們能夠大體分析出node中的事件循環的順序:
外部輸入數據-->輪詢階段(poll)-->檢查階段(check)-->關閉事件回調階段(close callback)-->定時器檢測階段(timer)-->I/O事件回調階段(I/O callbacks)-->閒置階段(idle, prepare)-->輪詢階段...
這些階段大體的功能以下: