在以前的開發過程當中遇到這樣的場景:javascript
頁面中有幾個功能區或者說模塊,他們每一個都有一個進度條,在頁面加載時會請求數據來渲染這幾個進度條,使之獨立展現不一樣的工程進度,因而在一個for循環中給每一個進度條綁定了一個定時器setInterval
,期待它能夠實現預期的效果。然而實際效果出乎意料,只有最後一個定時器實現了渲染正確數據的功能,前面的進度爲0。java
說到這裏,不少小夥伴可能已經猜到這裏面大體發生了什麼事情。這就是今天要說的,當for循環遇到了定時器,究竟發生了什麼事,又和異步、閉包有什麼關係。瀏覽器
首先,上述場景,能夠簡單抽象爲下面的一段代碼:網絡
for(var i=0;i<4;i++){
setTimeout(function(){
console.log(i)
},1000)
}
//4444複製代碼
有js基礎的小夥伴必定會一眼看出這段代碼的執行結果,那就是接連打印4個4。多線程
1.js是單線程(single threaded)語言,瀏覽器只分配給js一個主線程,用來執行任務(函數),但一次只能執行一個任務。這就造成了一個執行棧(execution context stack)。閉包
2.js的宿主環境(好比瀏覽器,Node)是多線程的,這就使得js具有了異步(asynchronous)的屬性。爲何要有異步屬性?簡單說就是主線程任務排隊執行,若是某些事件消耗時間過長,處理效率低下不說,還會致使頁面卡頓,因此要開闢「第二戰線」。異步
3.哪些事件是異步的?好比網絡請求,定時器和事件監聽,這些都是很是耗時的。他們都被放到了異步隊列中。這就造成了"任務隊列"(task queue)。咱們今天的主角,定時器,就在其中。async
因爲定時器是異步任務,按照js的事件執行機制,主線程即for循環,建立了四個定時器1234,他們所打印的i,因爲主線程已經結束,i=4,因此天然而然打印了4個4出來。函數
若是咱們但願有序打印0123這種不一樣的i值怎麼辦呢?oop
閉包小朋友頓時一臉懵逼,for循環和定時器好端端的,怎麼就跟我閉包搭上關係了呢?
是的,你沒聽錯,中央欽定了你來處理這件事。。。咳,嚴肅嚴肅,來看看怎麼回事。
咱們將代碼改進以下:
for (var i = 0; i < 4; i++) {
(function(a) {//閉包
setTimeout(function() {
console.log(a);//操縱變量a,和i無關
}, 3000);
})(i)
}
//0123
複製代碼
上面代碼將定時器放入一個自調用函數中,自調用函數傳入了for循環的i做爲實參賦予形參a,因此定時器打印這個a就拿到了每個i值0,1,2,3。不少博文也將這種自調用函數叫作「當即執行函數」,實際上是一回事。
爲何用一個自調用函數就能拿到每一個i的值了呢?仔細觀察能夠發現,這實際上是閉包在發揮做用。
在這裏,for循環裏定義的i變量其實暴露在全局做用域內,因而5個定時器裏的匿名函數它們其實共享了同一個做用域裏的同一個變量。因此若是想要0,1,2,3,4的結果,就要在每次循環的時候,把當前的i值單獨存下來,而咱們知道,閉包的特色是能夠保存數據,延長做用域鏈,匿名函數生成了閉包的效果,新建了一個做用域,這個做用域接收到每次循環的i值保存了下來,即便循環結束,閉包造成的做用域也不會被銷燬。這就是每一個i值能被單獨保存下來的緣由。
上面代碼還能夠改寫成下面的模樣,這也是比較常見的一種寫法。
for (var i = 0; i < 4; i++) {
setTimeout(fn(i), 3000);}
function fn(a){
return function(){
console.log(a);
}
}複製代碼
使用let關鍵字聲明for循環的i變量,不須要藉助閉包,便可實現上述函數效果。
for(let i = 0; i < 4; i++) {
setTimeout(function () {
console.log(i);
});
}
//0,1,2,3複製代碼
咱們知道,let關鍵字是ES6中一個至關大的變化,使用let關鍵字聲明變量,克服了以前使用var聲明的內存泄漏、全局污染等問題。
更重要的是,let能夠和{ }代碼塊結合造成塊級做用域。在for循環中使用let聲明計數變量i,i 只在本輪循環有效,至關於每一輪都會從新聲明一個 i。並且JS引擎會記住上一輪的 i,隨後的每一個循環都會使用上一個循環結束時的值來初始化這個變量i。
因此在for循環中使用let是至關不錯的選擇。
這倆兄弟其實很是類似,只不過一個是一次性的,一個是循環執行。
一般咱們使用setInterval時,傳遞的參數都是兩個,一個是回調函數callback,一個是延遲或者間隔時間delay,事實上,它是能夠傳遞多個參數的,舉例以下:
setInterval(function(msg1,msg2,...){},1000,'回調參數1','回調參數2',...);複製代碼
定時器延遲時間delay後面的參數,都會做爲前面回調函數的實參傳入。利用這個特色,咱們結合前面的閉包和異步的話題,稍加延伸。
當在for循環中傳入計數參數i給定時器的回調函數,會發生什麼事情?
下面這段代碼,打印結果如何呢?
function fn2() {
for (var i = 0; i < 4; i++) {
var tc = setInterval(function (i) {
console.log(i);
}, 1000, i);
}
}
//打印結果 012301230123循環複製代碼
爲何會打印出這個結果呢?
簡單說來,這仍然是一個閉包,這個閉包造成的關鍵,就是for循環計數參數i做爲定時器定時器的回調函數實參,傳入了回調函數。從這個角度看,這仍然是一個典型的閉包,即內部函數拿到了外部函數中定義的變量,而且一旦拿到,這個變量就會被保存。
for循環建立四個定時器後,主線程結束,開始處理異步任務,此時四個定時器1234處於「任務隊列」(task queue)中,主線程空閒,異步任務當即被推入主線程開始處理(這只是大體過程,實際過程還能夠細分,這裏再也不贅述)。此時四個定時器各自保存了一個i值分別是0123,他們遵循前後順序,每隔一秒各自打印本身保存的i值。這就是上面結果的由來。
至此,小夥伴們是否是已經明白了for循環、定時器相結合時發生了什麼事情呢?相信你已經小雞啄米般點頭,「懂了」「懂了」。好的,那,
來一波騷操做,當咱們在打印 i 值後面當即清除定時器,會發生什麼事情?
代碼以下:
function fn2() {
for (var i = 0; i < 4; i++) {
var tc = setInterval(function (i,tc) {
console.log(i);
clearInterval(tc)
}, 1000, i,tc);
}
}
//打印結果:0123333333 3無限循環複製代碼
????發生了什麼事情??剛剛創建的定時器大廈好像出現了一絲顫動。可是,這好像又是常規操做,定時器老是要清的,至於在哪清,那是另外一回事了。
不少同窗一看,這不和上面剛說的i造成閉包的原理同樣嘛,tc傳入做爲實參,因此每一個tc都被清除了,可是最後一個沒被清除是怎麼回事??
這裏更多的是考察對js執行機制和運算的理解。
定時器是有id的,在這裏依次爲1234 。當第一個定時器 tc=1
開始執行時,打印i爲0,接着清除定時器,問題來了,這個tc,是哪一個tc?這就是整個問題的關鍵。
事實上, =
是賦值運算符,先計算右邊,右邊計算時,這個tc值是多少呢?是 =
左邊剛剛新鮮熱辣var出來的tc1嗎?答案顯然是否認的。若是咱們此時打印查看這個tc值,會發現,它是
undefined。
緣由很簡單,此時還不存在 =
左邊的tc,在整個做用域內,找不到tc可清理。
接下來就簡單了,當第二個定時器開始執行時,狀況有變,此時 i=1
,清理定時器tc,這個tc是哪一個?很顯然,它是第一個 tc=1
。
至此,水落石出,當第四個定時器 tc=4
開始工做時, i=3
,清除的是上一個定時器 tc=3
,而它自己,沒有下一個定時器清除了,因此它會一直打印3。
本文由一個工程實際問題,抽象出函數模型,目的是探討一些在for循環中定時器帶來的一些問題和現象。其中涉及到事件循環(Event Loop)的部分簡單帶過,說的可能不夠嚴謹,建議有興趣的童鞋參閱相關文章作更全面的瞭解。文中若有錯誤,懇請指正。
,