從一道執行題,瞭解瀏覽器中JS執行機制

ntscshen發表於2018-05-30

執行題

setTimeout(function(){
    console.log('定時器開始啦')
});
new Promise(function(resolve){
    console.log('馬上執行for迴圈啦');
    for(var i = 0; i < 10000; i++){
        if(i == 99) resolve();
    }
}).then(function(){
    console.log('執行then函式啦');
});
console.log('程式碼執行結束');
// 馬上執行for迴圈啦
// 程式碼執行結束
// 執行then函式啦
// 定時器開始啦
// 如果沒有執行對,請往下看
複製程式碼

JavaScript本身是一門單執行緒語言

為什麼 JS 是單執行緒的?作為瀏覽器指令碼語言,JavaScript 的主要用途是與使用者互動,以及操作 DOM 。這決定了它只能是單執行緒,否則會帶來很複雜的同步問題。比如,假定 JavaScript 同時有兩個執行緒,一個執行緒在某個 DOM 節點上新增內容,另一個執行緒刪除了這個節點,這時瀏覽器應該以哪個執行緒為準?

同步執行和非同步執行

單執行緒就意味著,所有任務都需要排隊,前一個任務結束,才能執行後一個任務。如果前一個任務耗時很長,那麼後一個任務就不得不一直等待 於是乎,JS 設計者們把所有任務分成兩類,同步和非同步

  1. 同步:只有前一個任務執行完畢,才能執行後一個任務
  2. 非同步:當同步任務執行到某個 WebAPI 時,就會觸發非同步操作,此時瀏覽器會單獨開執行緒去處理這些非同步任務。

任務佇列、回撥佇列、事件迴圈

WebAPI 是啥?瀏覽器事件、定時器、ajax,這些操作不會阻塞 JS 的執行,JS 會跳過當前程式碼,執行後續程式碼

  1. 任務佇列( Task Queue ):主執行緒執行完畢後所觸發的非同步任務( WebAPIs ),叫任務佇列
  2. 回撥佇列( Callback Queue ):這些非同步 WebAPI 執行完成後得到的結果,會新增到 callback queue
  3. 事件迴圈( Event Loop ):只要主執行緒的同步任務執行完畢,就會不斷的讀取 "回撥佇列" 中的回撥函式,到主執行緒中執行,這個過程不斷迴圈往復

如何知道主執行緒執行執行完畢?JS引擎存在 monitoring process 程式,會持續不斷的檢查主執行緒執行為空,一旦為空,就會去 callback queue 中檢查是否有等待被呼叫的函式。

說了一堆概念,來一起看看這段程式碼

console.log('1');
setTimeout(function() {
    console.log('2');
}, 0);
console.log('3');
複製程式碼

執行結果如下:

  1. 列印1
  2. 遇到 WebAPI( setTimeout ) ,瀏覽器新開定時器執行緒處理,執行完成後把回撥函式存放到回撥佇列中。專業一點的說發: JS 引擎遇到非同步任務後不會一直等待其返回結果,而是將這個任務掛起交給其他瀏覽器執行緒處理,自己繼續執行主執行緒中的其他任務。這個非同步任務執行完畢後,把結果返回給回撥佇列。被放入的程式碼不會被立即執行。而是當主執行緒所有同步任務執行完畢, monitoring process 程式就會把 "回撥佇列" 中的第一個回撥程式碼放入主執行緒。然後主執行緒執行程式碼。如此反覆
  3. 列印3 非同步 setTimeout 不會阻塞同步程式碼,因此會首先列印3
  4. 主執行緒執行完畢後,執行 Callback Queue 列印2

macro task 與 micro task

非同步任務的執行優先順序並不相同,它們被分為兩類:微任務( micro task ) 和 巨集任務( macro task ) 根據非同步事件的型別,這些事件實際上會被派發對應的巨集任務和微任務中,在當前主執行緒執行完畢後,

  1. 會先檢視微任務中是否有事件存在,如果不存在,則再去找巨集任務
  2. 如果存在,則會依次執行佇列中的引數,直到微任務列表為空,讓後去巨集任務中一次讀取事件到主執行緒中執行,如此反覆 當前主執行緒執行完畢後,會首先處理微任務佇列中的事件,讓後再去讀取巨集任務佇列的事件。在同一次事件迴圈中,微任務永遠在巨集任務之前執行。
  1. 巨集任務( macro-task ):整體 scriptsetTimeoutsetIntervalUI互動事件I/O
  2. 微任務( micro-task ):process.nextTickPromiseMutaionObserver

整體script本身就是一次巨集任務

上程式碼

(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function (resolve, reject) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()
1. setTimeout:巨集任務:存入巨集任務佇列
2. Promise:函式本身是同步執行的( **Promise** 只有一個引數,預設new的時候就會同步執行), `.then` 是非同步,因此依次列印12  `.then` 存入微任務中
3. 列印3( 第一次主執行緒執行完畢 )
4. 執行微任務中的回撥函式:5, 讓後執行巨集任務中的 `setTimeout` 4
// 最終結果1,2,3,5,4
複製程式碼

來點稍微高難度的

console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

setTimeout(() => {
    console.log(9)
    new Promise(resolve => {
        console.log(11)
        resolve()
    }).then(() => {
        console.log(12)
    })
})
第一次同步執行:1,7,8
    巨集任務:setTimeout
2,4,5
9,11,12
複製程式碼

應用場景

console.log('先執行這裡');
setTimeout(() => {
    console.log('再執行啦');
}, 0);
複製程式碼
  1. 在工作中,經常會遇到上述的程式碼,含義:只要主執行緒執行完成,就立馬執行 setTimeout 中的回撥程式碼
  2. micro-task 優先於 macro-task 執行,在瀏覽器中高優先順序的程式碼可以在 promisesetTimeout(()=>{}, 0) 中執行
  3. 認知尚淺,只能想到這些應用場景

總結

這些概念是中級以上前端開發必知必會內容,其實實際的落地場景很少

  1. 為什麼 new Promise 第一個引數是同步執行的 ?學習Promise && 簡易實現Promise
  2. node 中的 JS 執行機制是什麼樣子的?從一道執行題,瞭解Node中JS執行機制

附:這篇部落格 也許 想表達 瀏覽器環境中JS同步和非同步的執行過程 (⊙﹏⊙)b

相關文章