【學習筆記】JS經典異步操作,從閉包到async/await


參考文獻:王仕軍——知乎專欄前端周刊

感謝作者的熱心總結,本文在理解的基礎上,根據自己能力水平作了一點小小的修改,在加深自己印象的同時也希望能和各位共同進步...

 1. 異步與for循環

拋出一個問題,下面的代碼輸出什么?

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

相信絕大部分同學都能答的上,它的正確答案是立即輸出5,過1秒鍾后一次性輸出5個5,這是一個典型的JS異步問題,首先for循環的循環體是一個異步函數,並且變量i添加到全局環境中,所以立即輸出一個5,一秒鍾后,異步函數setTimeout輸出五次循環的結果,打印5 5 5 5 5(沒有時間間隔)。

2. 閉包

現在我們把需求改一下,希望輸出的結果是5 ->0,1,2,3,4, 應該怎么修改代碼呢?

很明顯我們可以用閉包創建一個不銷毀的作用域,保證變量i每次都能正常輸出。 

1 for(var i=0;i<5;i++){
2     (function(j)
3         {setTimeout(() => {
4             console.log(j); //過一秒輸出 0,1,2,3,4
5     }, 1000)})(i)
6 }
7 console.log(i);  //立即輸出5

因為立即執行會造成內存泄漏不建立大量使用,那么我們還可以這樣

var output = function(i){
    setTimeout(()=>{
        console.log(i);  // 過1秒輸出0,1,2,3,4
    },1000)
}
for(var i=0;i<5;i++){
    output(i);
}
console.log(i);  //立即輸出5

JS基本類型是按值傳遞的,我們給函數output傳了一個參數,所以它就會保存每次循環的實參,所以得到的結果和采用立即執行函數的結果一致。

3. ES6語法

當然我們也可以使用ES6的語法,還記得for循環中使用let聲明可以有效阻止變量添加到全局作用域嗎?

1 for(let i=0;i<5;i++){
2     setTimeout(()=>{
3         console.log(i)  //一秒鍾后同時輸出0,1,2,3,4
4     },1000)
5 }
6 console.log(i) //這一行會報錯,因為i只存在於for循環中

for循環中let聲明有一個特點,i只在本輪循環中有效,所以每循環一個i其實都是新變量,而javaScript引擎內部會記住上一次循環的值,初始化變量i時,就在上輪循環基礎上計算。

現在我們又改一下需求,希望先輸出0,之后每隔一秒依次輸出1,2,3,4,循環結束再輸出5。

很容易想到,我們可以再增加一個定時器,定時器的時間和循環次數有關

 1 for(var i=0;i<5;i++){
 2     (function(j){
 3         setTimeout(() => {
 4             console.log(j)  //立即輸出0,之后每隔1秒輸出1,2,3,4
 5         }, 1000*j);
 6     })(i)
 7 }
 8 setTimeout(()=>{
 9     console.log(i)  //循環結束輸出5
10 },1000*i)

這雖然也是個辦法,但代碼寫着確實不太好看,異步操作我們首先就要想到Promise對象,嘗試用Promise對象來改寫

let tasks = [];
for(var i=0;i<5;i++){
    ((j)=>{
        tasks.push(new Promise(
            (resolve)=>{
                setTimeout(() => {
                    console.log(j);
                    resolve();       //執行resolve,返回Promise處理結果
                }, 1000*j);
            }
        ))
    })(i)
}
Promise.all(tasks).then(()=>{
    setTimeout(() => {
        console.log(i);    
    }, 1000);                //只要把時間設為1秒
})

Promise.all返回一個Promise實例,在tasks的promise狀態為resolved時回調完成,這就是我們必須要在循環體中resolve()的原因。

我們將上面的代碼重新排版,讓其顆粒度更小,模塊化更好,簡潔明了

let tasks = [];   //存放一個異步操作
let output = (i)=>  //返回一個Promise對象
    new Promise((resolve)=>{
        setTimeout(() => {
            console.log(i);
            resolve();
        }, 1000*i);
    })
for(var i=0;i<5;i++){     //生成全部的異步操作
    tasks.push(output(i))
}
Promise.all(tasks).then(()=>{   //tasks里的promise對象都為resolved調用then鏈的第一個回調函數
    setTimeout(() => {
        console.log(i)
    }, 1000);
})

4. async/await優化

上次寫了一篇關於async和await優化then鏈的博客,感興趣的可以看看:深入理解async/await

對於then鏈,我們是可以進一步優化的:

let sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});

(async () => {  // 聲明即執行的 async 函數表達式
    for (var i = 0; i < 5; i++) {
        await sleep(1000);
        console.log(i);
    }
    await sleep(1000);
    console.log(i);
})();


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM