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,如果想要每隔一秒輸出這樣的結果,可以將程式修改為如下:
2、閉包的方法for (let i = 0; i < 5; ++i) { setTimeout(function() { console.log(i); }, i*1000); }
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博大精深,不是一句話就能概括出來的。