參考文獻:王仕軍——知乎專欄前端周刊
感謝作者的熱心總結,本文在理解的基礎上,根據自己能力水平作了一點小小的修改,在加深自己印象的同時也希望能和各位共同進步...
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); })();
