js的執行機制

js在哪執行

js的執行引擎基於v8(c++編寫),在chrome和node中都有應用,執行時有如下兩部分構成javascript

  • 內存堆(內存分配)
  • 調用棧(代碼執行)

上述兩部分的聯繫就是代碼在調用棧中執行,執行過程當中會存取一些對象在內存堆上。java

咱們寫的js代碼通過js引擎(解釋器)轉化爲高效的機器碼,如今的v8引擎由TurboFan和Ignition兩部分構成,其中Ignition是解釋器,而TurboFan主要對代碼作些優化,以提升執行性能。node

基於執行引擎的執行原理在代碼層面咱們能夠作些優化,能夠參考我以前的一篇文章c++

js如何執行

js同步執行

js按照代碼順序執行,在棧上分配執行空間,按照調用順序,會有出棧入棧等各類狀況,比較好分析,惟一值的說的地方就是js只有一個主線程,棧空間有限,若是遞歸執行過深會發生溢出,因此在編寫代碼層面須要注意這種狀況。chrome

js異步執行

爲何要有異步?

同步單線程代碼處理起來方便,代碼表達也容易,更符合咱們的思惟方式,爲何還會出現異步呢?
由於同步會發生阻塞,在如今這個高併發時代,不能很好的處理海量請求,同時也不能充分利用硬件資源(想一想cpu和io之間處理速度差別你就深有體會)。
可是爲何很少線程呢,例如java,主要是單個線程上運行代碼相對來多線程來講說容易寫,沒必要考慮在多線程環境中出現的複雜場景,例如死鎖等等。segmentfault

異步執行機制?

異步執行相對來講複雜些,因此詳細描述下,關鍵是在各類使用狀況下執行順序問題,在此就須要引入一個概念-->Event Loop。結合下面這幅圖進行大體說明下:
event-loop-gcyapi

Event Loop的概念
  • 全部任務在主線程上執行,造成一個執行棧(execution context stack),上圖stack區域所示。
  • 執行過程當中可能會調用異步api,其中Background Threads負責具體異步任務執行,結束後將宏任務回調邏輯放入task queue中,微任務回調邏輯放入micro task隊列中。
  • 主線程執行完畢,檢查microtask隊列是否爲空,會執行到隊列空爲止
  • 從宏任務隊列中取出一個在執行,執行完後,檢查並取出執行microtask隊列的任務,而後不斷重複這個步驟,對於這整個循環過程,一個對應的描述名詞就叫作event loop。

node中異步

異步任務分類promise

  • macrotask類型包括 script總體代碼,setTimeout,setInterval,setImmediate,I/O……
  • microtask類型包括 Promise process.nextTick Object.observe MutaionObserver……

node中event loop各個階段的操做以下圖所示session

node-event-loop
說明,上圖中每一個盒子表示了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...

相關文章
相關標籤/搜索