js的執行引擎基於v8(c++編寫),在chrome和node中都有應用,執行時有如下兩部分構成javascript
- 內存堆(內存分配)
- 調用棧(代碼執行)
上述兩部分的聯繫就是代碼在調用棧中執行,執行過程當中會存取一些對象在內存堆上。java
咱們寫的js代碼通過js引擎(解釋器)轉化爲高效的機器碼,如今的v8引擎由TurboFan和Ignition兩部分構成,其中Ignition是解釋器,而TurboFan主要對代碼作些優化,以提升執行性能。node
基於執行引擎的執行原理在代碼層面咱們能夠作些優化,能夠參考我以前的一篇文章c++
js按照代碼順序執行,在棧上分配執行空間,按照調用順序,會有出棧入棧等各類狀況,比較好分析,惟一值的說的地方就是js只有一個主線程,棧空間有限,若是遞歸執行過深會發生溢出,因此在編寫代碼層面須要注意這種狀況。chrome
同步單線程代碼處理起來方便,代碼表達也容易,更符合咱們的思惟方式,爲何還會出現異步呢?
由於同步會發生阻塞,在如今這個高併發時代,不能很好的處理海量請求,同時也不能充分利用硬件資源(想一想cpu和io之間處理速度差別你就深有體會)。
可是爲何很少線程呢,例如java,主要是單個線程上運行代碼相對來多線程來講說容易寫,沒必要考慮在多線程環境中出現的複雜場景,例如死鎖等等。segmentfault
異步執行相對來講複雜些,因此詳細描述下,關鍵是在各類使用狀況下執行順序問題,在此就須要引入一個概念-->Event Loop。結合下面這幅圖進行大體說明下:api
Event Loop的概念
- 全部任務在主線程上執行,造成一個執行棧(execution context stack),上圖stack區域所示。
- 執行過程當中可能會調用異步api,其中Background Threads負責具體異步任務執行,結束後將宏任務回調邏輯放入task queue中,微任務回調邏輯放入micro task隊列中。
- 主線程執行完畢,檢查microtask隊列是否爲空,會執行到隊列空爲止
- 從宏任務隊列中取出一個在執行,執行完後,檢查並取出執行microtask隊列的任務,而後不斷重複這個步驟,對於這整個循環過程,一個對應的描述名詞就叫作event loop。
異步任務分類promise
- macrotask類型包括 script總體代碼,setTimeout,setInterval,setImmediate,I/O……
- microtask類型包括 Promise process.nextTick Object.observe MutaionObserver……
node中event loop各個階段的操做以下圖所示session
說明,上圖中每一個盒子表示了event loop的一個階段,每一個階段執行完畢後,或者執行的回調數量達到上限後,event loop會進入下個階段。多線程
timers: 在達到這個下限時間後執行setTimeout()和setInterval()這些定時器設定的回調。 I/O callbacks: 執行除了close回調,timer的回調,和setImmediate()的回調,例如操做系統回調tcp錯誤。 idle, prepare: 僅內部使用。 poll: 獲取新的I/O事件,例如socket的讀寫事件;node會在適當條件下阻塞在這裏,若是poll階段空閒,纔會進入下一階段。 check: 執行setImmediate()設定的回調。 close callbacks: 執行好比socket.on('close', ...)的回調。
下面結合一些具體例子進行說明
require('fs').readFile('./case1.js', () => { setTimeout(() => { console.log('setTimeout in poll phase'); }); setImmediate(() => { console.log('setImmediate in poll phase'); }); });
輸出結果是: setImmediate in poll phase setTimeout in poll phase Process finished with exit code 0
說明 setImmediate的回調永遠先執行,由於readFile的回調執行是在 poll 階段,因此接下來的 check 階段會先執行 setImmediate 的回調。
setTimeout(() => console.log('setTimeout1'), 1000); setTimeout(() => { console.log('setTimeout2'); process.nextTick(() => console.log('nextTick1')); }, 0); setTimeout(() => console.log('setTimeout3'), 0); process.nextTick(() => console.log('nextTick2')); process.nextTick(() => { process.nextTick(console.log.bind(console, 'nextTick3')); }); Promise.resolve('xxx').then(() => { console.log('promise'); testPromise(); }); process.nextTick(() => console.log('nextTick4'));
結果是:
nextTick2 nextTick4 nextTick3 promise setTimeout2 setTimeout3 nextTick1 setTimeout1
在描述什麼是event loop中,大概描述了microtask機制,但具體到nextTick比較特別,有一個Tick-Task-Queue專門用於存放process.nextTick的任務,且有調用深度限制,上限是1000。js引擎執行 Macro Task 任務結束後,會先遍歷執行Tick-Task-Queue的全部任務,緊接着再遍歷 Micro Task Queue 的全部任務。具體執行邏輯能夠下面代碼表示。
for (macroTask of macroTaskQueue) { // 1. Handle current MACRO-TASK handleMacroTask(); // 2. Handle all NEXT-TICK for (nextTick of nextTickQueue) { handleNextTick(nextTick); } // 3. Handle all MICRO-TASK for (microTask of microTaskQueue) { handleMicroTask(microTask); } }
因此纔會先輸出process.nextTick而後纔會是promise,其它的輸出順序不在贅述,前面講event-loop機制時已經說明了。根據上面代碼表述的執行邏輯,很顯然能夠獲得下面的個結論,當遞歸調用時會發生死循環,而宏任務就不會。
testPromise(); function testPromise() { promise = Promise.resolve('xxx').then(() => { console.log('promise'); testPromise(); }); } //將以前步驟的promise任務換成這個,setTimeout2以及以後的輸出永遠沒機會出來,類比到nextTick也是這種效果
看了一些書,參考了不少資料,將本身學習的東西,理解後在輸出,但願你們辯證的看待,有空的話接下來研究一下源碼,畢竟經過demo驗證結論的說服力沒有源碼來的那麼直接。
參考連接
https://jakearchibald.com/201...
https://blog.sessionstack.com...
https://cnodejs.org/topic/592...
https://developer.mozilla.org...
https://nodejs.org/en/docs/gu...