『媽媽不再用擔憂之』JavaScript執行機制

JavaScript執行機制

JavaScript是一門單線程語言,一切JavaScript中的「多線程」都是用單線程模擬出來的,而這主要是靠事件循環機制來實現的javascript

事件循環

事件循環導圖

  • 同步和異步任務分別進入不一樣的執行場所,同步任務進入主線程中,而異步任務則進入Event Table並註冊函數
  • 當指定的事情(例如ajax事件)完成時,Event Table會將這個函數移入Event Queue
  • 當主線程內的任務執行完畢時,會去Event Queue讀取對應的函數,將其放入主線程中執行
  • 上述過程會不斷重複,整個過程便是事件循環Event Loop

JS引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就回去Event Queue那裏檢查是否有等待被調用的函數。前端

show demo

let data = {};
$.ajax({
    url: www.javascript.com,
    data: data,
    success: () => {
        console.log("發送成功!");
    }
})
console.log("同步代碼執行結束");
複製代碼
  • 執行data變量聲明賦值操做
  • 碰到ajax,發現是個異步任務,將這個任務放入Event Table,註冊回調函數success
  • 繼續向下執行console.log("同步代碼執行結束");
  • ajax事件完成,回調函數success進入Event Queue。
  • 主線程任務執行完畢,從Event Queue中讀取success並執行

setTimeout

setTimeout這個函數是通過指定時間後,把要執行的任務,加入到Event Queue中。又由於主線程任務是一個個執行,若是前面的任務執行須要的時間太長,那麼Event Queue中的任務只能等着,因此可能會致使setTimeout要執行的任務真正的延遲時間會大於指定時間。java

show demo

setTimeout(() => {
    task()
},3000)

sleep(10000000)
複製代碼
  • task進入Event Table並註冊,計時開始
  • 執行sleep函數,很慢,很是慢,計時還在繼續
  • 3秒到了,計時事件timeout完成,task進入Event Queue,可是由於sleep執行得太慢了,還沒執行完,只好等着
  • sleep終於執行完了,task終於從Event Queue進入到主線程中執行
setTimeout(fn, 0) // 指定某個任務在主線程執行棧爲空後立刻執行,但根據HTML的標準,實際不可能達到0毫秒,最低是4毫秒
複製代碼

setInterval

setIntervalsetTimeout相似,不一樣點在於,setInterval是循環的執行。它會每隔指定的時間,將註冊的函數置入Event Queue,若是前面的任務耗時過久,那麼一樣須要等待。 對於setInterval(fn, ms)來講,不是每過ms秒會執行一次fn,而是每過ms秒,會有fn進入Event Queue。一旦setInterval的回調函數fn執行時間超過了延時時間ms,那麼就徹底看不出來有時間間隔了node

更精細任務的定義

除了廣義的同步任務和異步任務,還有對任務有更精確的定義ajax

  • macro-task(宏任務):包括總體代碼script, setTimeout, setInterval
  • micro-taks(微任務):Promise, process.nextTick

process.nextTick相似於node版的setTimeout,在下次循環的下一次循環中調用callback回調函數promise

不一樣類型的任務會進入對應的Event Queue,例如setTimeoutsetInterval會進入相同的Event Queuebash

事件循環的順序,決定JS代碼的執行順序。進入總體代碼(宏任務)後,開始第一次循環。接着執行這一次循環所產生的全部微任務。而後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行全部的微任務。多線程

事件循環&宏任務&微任務

show demo

setTimeout(function() {
    console.log('setTimeout');
})

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})

console.log('console');
複製代碼
  • 這段代碼做爲宏任務,進入主線程。
  • 先遇到setTimeout,那麼將其回調函數註冊後分發到宏任務Event Queue。(註冊過程與上同,下文再也不描述)
  • 接下來遇到了Promisenew Promise當即執行,並將then函數分發到微任務Event Queue。
  • 遇到console.log(),當即執行。
  • 總體代碼script做爲第一個宏任務執行結束,看看有哪些微任務?咱們發現了then在微任務Event Queue裏面,執行。
  • ok,第一輪事件循環結束了,咱們開始第二輪循環,固然要從宏任務Event Queue開始。咱們發現了宏任務Event Queue中setTimeout對應的回調函數,當即執行。
  • 結束。

檢驗你是否真正的掌握了js的執行機制

題1:異步

async function async1() {
  console.log(1);
  const result = await async2();
  console.log(3);
}

async function async2() {
  console.log(2);
}

Promise.resolve().then(() => {
  console.log(4);
});

setTimeout(() => {
  console.log(5);
});

async1();
console.log(6);
複製代碼

答案: [1,2,6,4,3,5]async

  • 整段代碼做爲宏任務,進入主線程
  • 遇到兩個async函數聲明,先無論。接着遇到一個Promise, 將then函數分發到微任務Event Queue
  • 遇到setTimeout,將其回調函數註冊後分發到宏任務Event Queue
  • 進入async1函數並執行,輸出1
  • 往下,遇到await並執行async2輸出2,await意味着後面的代碼要等等了
  • console.log(3)是在async2函數返回的Promise的then函數中執行的,因此講它分發到微任務Event Queue
  • 此時主線程中宏任務執行完畢,開始按順序執行微任務,先輸出4,接着輸出3
  • 第一次事件循環完畢,開始第二次事件循環,開始執行新同步任務,輸出5
  • 結束

題2:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
複製代碼

答案: [1,7,6,8, 2,4,3,5, 9,11,10,12]

第一輪事件循環流程分析以下:

  • 總體script做爲第一個宏任務進入主線程,遇到console.log,輸出1。
  • 遇到setTimeout,其回調函數被分發到宏任務Event Queue中。咱們暫且記爲setTimeout1
  • 遇到process.nextTick(),其回調函數被分發到微任務Event Queue中。咱們記爲process1
  • 遇到Promisenew Promise直接執行,輸出7。then被分發到微任務Event Queue中。咱們記爲then1
  • 又遇到了setTimeout,其回調函數被分發到宏任務Event Queue中,咱們記爲setTimeout2
宏任務Event Queue 微任務Event Queue
setTimeout1 process1
setTimeout2 then1
  • 上表是第一輪事件循環宏任務結束時各Event Queue的狀況,此時已經輸出了1和7。
  • 咱們發現了process1then1兩個微任務。
  • 執行process1,輸出6。
  • 執行then1,輸出8。

好了,第一輪事件循環正式結束,這一輪的結果是輸出1,7,6,8。那麼第二輪時間循環從setTimeout1宏任務開始:

  • 首先輸出2。接下來遇到了process.nextTick(),一樣將其分發到微任務Event Queue中,記爲process2new Promise當即執行輸出4,then也分發到微任務Event Queue中,記爲then2
宏任務Event Queue 微任務Event Queue
setTimeout2 process2
then2
  • 第二輪事件循環宏任務結束,咱們發現有process2then2兩個微任務能夠執行。
  • 輸出3。
  • 輸出5。
  • 第二輪事件循環結束,第二輪輸出2,4,3,5。
  • 第三輪事件循環開始,此時只剩setTimeout2了,執行。
  • 直接輸出9。
  • process.nextTick()分發到微任務Event Queue中。記爲process3
  • 直接執行new Promise,輸出11。
  • 將then分發到微任務Event Queue中,記爲then3
宏任務Event Queue 微任務Event Queue
process3
then3
  • 第三輪事件循環宏任務執行結束,執行兩個微任務process3then3
  • 輸出10。
  • 輸出12。
  • 第三輪事件循環結束,第三輪輸出9,11,10,12。

整段代碼,共進行了三次事件循環,完整的輸出爲1,7,6,8,2,4,3,5,9,11,10,12。 (請注意,node環境下的事件監聽依賴libuv與前端環境不徹底相同,輸出順序可能會有偏差)

感謝

這一次,完全弄懂 JavaScript 執行機制

相關文章
相關標籤/搜索