一道setTimeout async promise執行順序的筆試題引發的思考

DaYu發表於2019-02-16

====據說這是今日頭條去年的一道筆試題,主要考察的是setTimeout async promise執行順序

~先雙手奉上這道題目~

   async function async1() {
            console.log("async1 start");
            await  async2();
            console.log("async1 end");
 
        }
        async  function async2() {
           console.log( `async2`);
        }
        console.log("script start");
        setTimeout(function () {
            console.log("settimeout");
        },0);
        async1();
        new Promise(function (resolve) {
            console.log("promise1");
            resolve();
        }).then(function () {
            console.log("promise2");
        });
        console.log(`script end`);
 
  • 首先,我們先來了解幾個概念:

    JS眾所周知是單執行緒語言,Javascript引擎同一時刻只能執行一個程式碼塊,使用Event Loop作為它的非同步執行機制

  • 那麼Event Loop是如何實現非同步呢,個人淺顯的理解如下:

    1. 同步程式碼按照上下文的順序放進主程式中去執行
    2. 非同步函式放進非同步佇列中,等待執行,在非同步佇列執行的順序按照先進先出的原則
    3. 等主程式中的同步函式執行完畢後,輪詢去執行非同步佇列中的非同步函式

      ⚠️注意: setTimeOut並不是直接的把你的回掉函式放進上述的非同步佇列中去,而是在定時器的時間到了之後,把回掉函式放到執行非同步佇列中去。如果此時這個佇列已經有很多工了,那就排在他們的後面。這也就解釋了為什麼setTimeOut為什麼不能精準的執行的問題了。setTimeOut執行需要滿足兩個條件:

      1. 主程式必須是空閒的狀態,如果到時間了,主程式不空閒也不會執行你的回掉函式 
      2. 這個回掉函式需要等到插入非同步佇列時前面的非同步函式都執行完了,才會執行
      
  • 理解了Eventloop非同步實現的方式,再來補充一下promise、async/await

    1. 首先,new Promise是同步的任務,會被放到主程式中去立即執行。而.then()函式是非同步任務會放到非同步佇列中去,那什麼時候放到非同步佇列中去呢?當你的promise狀態結束的時候,就會立即放進非同步佇列中去了。如果你要問他和setTimeOut誰當進去的快,要從下面兩個方面考慮:

      1. promise結束時。.then內函式插入非同步佇列的時間與setTimeOut的回掉函式插入佇列的時間,誰的早,誰的就最快
      2. **如果promise是同步的而setTimeOut時間是0,那麼是promise先執行**。至於什麼,查了很多的資料,瞭解到:一個瀏覽器環境只能有一個事件迴圈,而一個事件迴圈可以有多個任務佇列。settimeout所在的佇列與promise.then()的佇列不同,面對此種情況,v8實現的時候會先從promise.then()的佇列取任務,但是並沒有很理解,如果有大佬願意指點迷津,請留言告知?
    2. 帶async關鍵字的函式會返回一個promise物件,如果裡面沒有await,執行起來等同於普通函式;如果沒有await,async函式並沒有很厲害是不是
    3. await 關鍵字要在 async 關鍵字函式的內部,await 寫在外面會報錯;await如同他的語意,就是在等待,等待右側的表示式完成。此時的await會讓出執行緒,阻塞async內後續的程式碼,先去執行async外的程式碼。等外面的同步程式碼執行完畢,才會執行裡面的後續程式碼。就算await的不是promise物件,是一個同步函式,也會等這樣操作

接下來,帶著上面的那些總結,步入正題

分析上述的程式碼,給任務型別分類

async function async1() {
    console.log( `async1 start’ ) // 同步程式碼2
    await async2()  // 執行async2
    console.log( `async1 end’ )   // 需要等async1外面的程式碼執行完,並且async2也執行完才會執行
}
async function async2() {
    console.log( `async2’ )  // 同步程式碼3
}

// ===================== 從這裡開始了表演,所以在這裡走起 ===============================

console.log( `script start’ )    // 同步程式碼1

setTimeout( function () {
 //  setTimeout放入event-loop中的macro-tasks佇列,暫不執行
console.log( `setTimeout` )
}, 0 )

async1()  //  如果有await,第一個【await前面的程式碼】屬於主程式執行—看最上面函式內分析

new Promise( function ( resolve ) {  // 注意這個方法裡面的是同步
             // 同步程式碼4
    console.log( `promise1’ ) 
    resolve();
} ).then(  
    //  .then()放入event-loop中的micro-tasks佇列
     function () {
        console.log( `promise2` )
    } 
)

console.log( `script end’ )  // 同步程式碼5
  1. 最先輸出的是同步程式碼,按照上下文執行順序是

    `script start’
    `async1 start’
    `async2’ 
    `promise1’ 
    `script end’
  2. 接下來,給非同步佇列執行的排序

    第一個執行的函式是async1() ,裡面有await,他要等待,等待就是要讓async外面的程式碼先執行,外面的那些同步程式碼執行好了之後,在執行這個async裡面的,await後面的程式碼執行。所以遇到await就讓出了執行緒,給到後面的程式碼,那麼下面的promise.then會先執行,後執行await後面的程式碼
    async1()的await後面和setTimeOut哪個先執行,這個要看await等待的是什麼。此處等待的是async2函式,這個函式裡面沒有await等待,帶async關鍵字的函式返回的是一個promise物件,所以async1中等待的是一個promise物件,並且這個promise物件裡面只有同步執行,會被放進時間迴圈的micro-tasks佇列,該佇列比setTimeOut的佇列先執行,執行完了之後還不能執行setTimeOut,因為執行完了之後就不阻塞主程式了,主程式要接著執行;也就是await後面的同步程式碼,要先去執行主程式。所以setTimeOut是在最後執行的
    再次驗證了上面所說的setTimeOut執行的必要條件之一是主程式空閒了
    因此,非同步程式碼的執行順序是

‘promise2`        // async函式外的程式碼先執行
`async1 end’    // -- await不阻塞了,async後面的同步程式碼
`setTimeout`   // promise.then的佇列比setTimeout佇列先執行

如果你真正的理解了Event Loop的執行機制,並且知道setTimeout的與promise.then並非一個佇列裡面的,那麼這道題就是很簡單的送分題

上述如有不足或者不當指出,請各位大佬匡正~~?

相關文章