【學習筆記】JS經典非同步操作,從閉包到async/await
參考文獻: 王仕軍——知乎專欄前端週刊
感謝作者的熱心總結,本文在理解的基礎上,根據自己能力水平作了一點小小的修改,在加深自己印象的同時也希望能和各位共同進步...
1. 非同步與for迴圈
丟擲一個問題,下面的程式碼輸出什麼?
1 for (var i = 0; i < 5; i++) { 2setTimeout(function() { 3console.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(() => { 4console.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++){ 2setTimeout(()=>{ 3console.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){ 3setTimeout(() => { 4console.log(j)//立即輸出0,之後每隔1秒輸出1,2,3,4 5}, 1000*j); 6})(i) 7 } 8 setTimeout(()=>{ 9console.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); })();