1. 程式人生 > >JavaScript同步、非同步、回撥執行順序分析

JavaScript同步、非同步、回撥執行順序分析

       之所以會寫這篇文章,是因為在做筆試題的時候,會遇到一題很經典的題目,關於setTimeout的輸出結果,下面我們先來看一道題目:

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

console.log(i);
     我相信只要是做過前端筆試題的都見過這樣的題目,那麼輸出的結果是什麼呢?

          第一種可能的答案:0 1 2 3 4 5 5

          第二種可能的答案:5 5 5 5 5 5 5(後面每個5隔一秒輸出)

    顯然第二種結果是正確的,接下來我們分析一下這個題目。首先看一下目前大家都在用的一個口令或者說方法:

          同步優先、非同步靠邊、回撥墊底

          用公式表達就是:同步 => 非同步 => 回撥

    現在根據這個口令我們來分析一下結果為什麼是這個:

    1)for迴圈和迴圈體外部的console是同步的,所以先執行for迴圈,再執行外部的console.log。(同步優先)

    2)for迴圈裡面有一個setTimeout回撥,他是墊底的存在,只能最後執行。(回撥墊底)

    那麼,為什麼我們最先輸出的是5呢?

    這個也是非常好理解,for迴圈先執行,但是不會給setTimeout傳參(回撥墊底),等for迴圈執行完,就會給setTimeout傳參,而外部的console打印出5是因為for迴圈執行完成了。

    那麼我們要如何能夠輸出0 1 2 3 4 5呢?

    目前我所知道的方法有兩種,第一種是利用let的方法,第二種是利用閉包的方法。

1、利用let

for (let i = 0; i < 5; ++i) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}
此時的輸出結果是:

0
1
2
3
4

但是現在是隔一秒之後依次輸出0 1 2 3 4 5,如果想要每隔一秒輸出這樣的結果,可以將程式修改為如下:

for (let i = 0; i < 5; ++i) {
    setTimeout(function() {
        console.log(i);
    }, i*1000);
}
2、閉包的方法
for (var i = 1; i <=20; i++){
    (function (i) {
        setTimeout(function timer() {
            console.log(i);
        }, i*1000)
    })(i);
}
     結果如上,這裡可以使用閉包的知識進行解釋,也可以用作用域輔助理解。

    由於 var i = xxx 是函式級別作用域,這裡通過一個立即函式將變數 i 傳入其中,使其包含在這一函式的作用域中。而在每次迴圈中,此立即函式都會將傳入的 i 值儲存下來,因而其迴圈展開結果為:

(function(){
    var count = 0;
    setTimeout( function timer() {
        console.log(count);
    }, count * 1000 );
})()
(function(){
    var count = 1;
    setTimeout( function timer() {
        console.log(count);
    }, count * 1000 );
})()
(function(){
    var count = 2;
    setTimeout( function timer() {
        console.log(count);
    }, count * 1000 );
})()
(function(){
    var count = 3;
    setTimeout( function timer() {
        console.log(count);
    }, count * 1000 );
})()
(function(){
    var count = 4;
    setTimeout( function timer() {
        console.log(count);
    }, count * 1000 );
})()
(function(){
    var count = 5;
    setTimeout( function timer() {
        console.log(count);
    }, count * 1000 );
})()

上面主要講了同步和回撥執行順序的問題,接著我就舉一個包含同步、非同步、回撥的例子。
let a = new Promise(
  function(resolve, reject) {
    console.log(1)
    setTimeout(() => console.log(2), 0)
    console.log(3)
    console.log(4)
    resolve(true)
  }
)
a.then(v => {
  console.log(8)
})

let b = new Promise(
  function() {
    console.log(5)
    setTimeout(() => console.log(6), 0)
  }
)

console.log(7)

     在看正確結果之前,我先進行分析題目(同步 => 非同步 => 回撥):

    1)看同步程式碼:a變數是一個Promise,一開始大家都會以為Promise不是非同步嗎?不不,其實Promise是非同步的,是指他的then()和catch()方法,Promise本身還是同步的,所以這裡先執行a變數內部的Promise同步程式碼。(同步優先)
    console.log(1)
    setTimeout(() => console.log(2), 0) //回撥
    console.log(3)
    console.log(4)
    2)Promise內部有4個console,第二個是一個setTimeout回撥(回撥墊底,所以暫時不輸出)。所以這裡先輸出1,3,4,回撥的方法丟到訊息佇列中排隊等著。
    3)接著執行resolve(true),進入then(),then是非同步,下面還有同步沒執行完呢,所以then也去訊息佇列排隊等候。(非同步靠邊)

    4)b變數也是一個Promise,和a一樣,同樣是同步的,執行內部的同步程式碼,輸出5,setTimeout是回撥,去訊息佇列排隊等候,這裡輸出5。
    5)最下面同步輸出7。
    6)現在同步的程式碼執行完了,JavaScript就跑去訊息佇列呼叫非同步的程式碼:非同步,出來執行了。這裡只有一個非同步then,所以輸出8。
   7)此時,非同步也over,輪到回撥函式:回撥,出來執行了。這裡有2個回撥在排隊,他們的時間都設定為0,所以不受時間影響,只跟排隊先後順序有關。則先輸出a裡面的回撥2,最後輸出b裡面的回撥6。
   8)最終輸出結果就是:1、3、4、5、7、8、2、6。

到這裡,解釋結束。關於執行順序還是多去實踐和理解,JavaScript博大精深,不是一句話就能概括出來的。