event loop ? microtask ? macrotask ? 不好意思,我記不住,關于這方面的解釋以及深入理解一大堆的中英結合,中間穿插著各種 setImmediate、requestAnimationFrame。‘我才學,腦袋大,通俗點,886 Ctrl+W’。
我想用俗話說
我先把主要圍繞的例子列出來,也主要是通過這個最常見代碼去解釋一下我對event loop的理解:
for(var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
},1000*i);
}
A:哇,你個水貨,先出來一個5 然后 一秒一個 5,面試經典題還用你教? (張學友的問候.jpg)
me: 好像還可以在深入一下....,比如:
- 為什么每次都是5;
- 為什么是你理解的1s一次;
- 為什么用立即執行函數(IIFE)就可以解決, 以及ES6中的let。
我想,等我廢話說完你可能會有個大概理解了,到時候我想基于這個理解再寫一篇關于AMD的白話文實現。
上道具
阻塞事件
js是單線程,在代碼執行階段對代碼進行分類處理,按照我的理解就是:當我找到你的時候,你就必須去做或者立刻告訴我結果。就好比我去訂餐,有沒有位子就在此次通話內告訴我。
那我們的例子來說就是:
for(var i = 0; i < 5; i++) {}
=> var i = 0; {做點什么}(i++); {做點什么}(i++); {做點什么}(i++); {做點什么}(i++);...
//當然會說了,明明還要 i<5 的判斷了,這個里判斷也算是阻塞。很小的一部分。
這里就是 就往 阻塞隊列增加了5個事件(當然肯定不止5個,每個事件內部還有事件),并按照先進先出(堆)的原則去執行。
非阻塞事件
當程序讀取到了非阻塞事件,將該事件如加入到當前的非堵塞隊列,等待同一個任務隊列中的阻塞隊列執行結束后,再去執行非阻塞隊列中的任務,當然也是先進先出
setTimeout(function(){console.log('1')},500); //A
setTimeout(function(){console.log('2')},100); //B
=>
2
1
不要以為B比A先出來,就以為B比A先執行。這個,肯定要講。
根據列子來說:
setTimeout(function() {
console.log(i);
},1000*i);
// 就是往 非阻塞隊列 添加了 五個非阻塞時間
// 這個時候我們一定要區分 console.log(i)與 setTimeout(fn,1000*i) 中的 i;
//當然他們是同一個i,只是時刻不同,那么時刻不用是什么?
- console.log(i)中的 i: 是當這個非阻塞事件可以執行的時候去獲取的 i;
- setTimeout(fn,1000*i)中的 i : 是注冊這個非阻塞事件并向非阻塞事件隊列添加的時候的 i,這個I就是0 1 2 3 4(這一個跟開始我拋出的,為什么是你理解的1秒1次5有關);
整體分析
- 首先必須明確的是,非阻塞隊列只有在對應的阻塞隊列執行完畢后才能執行。
- 非阻塞隊列,并不一定是一次循環執行就能結束的,也就是說按照注冊順序執行,滿足條件的事件執行回調并移除該隊列。
解答
為什么輸出console.log(i),輸出5個5;
因為所有非阻塞隊列中的fn0-fn4中都是 console.log(i);
根據js設計思想,只有當用到某個變量時,才去尋找這個變量的值,這個時候非阻塞隊列是在阻塞隊列執行完全之后再執行的;
那么這個時候的i已經經過了5個for循環5個i++從一開始的0變成了5,所以在fn0-fn5執行的時候console.log(i),i 的值 已經是5了。
為什么是你理解的1s一個5
首先它的確是 0s 5 ;1s 5 ;2s 5 ;3s 5 ;4s 5 ;
為什么又是1s一個5呢? 參照物
當你根據每個console .log(i)出現為基準,就如同大家都在一個房間,A出門走1步,B出門走2步,C出門走3步。。。
相對于ABC...每個人來說,他們之間的確相差了1步,
但是相對這個房間來說,ABC...每個人相對于房間可就不是固定的1步了。
我在阻塞隊列注冊了五個非阻塞事件,誰告你這幾個setTimeout之間有關系的。。。,
我一個人玩的setTimeout為什么要根據你的setTimeout結束才能玩?
解答:
在阻塞隊列執行的時候,注冊了五個非阻塞事件,例如:
阻塞事件中通過setTimeout注冊了一個非阻塞事件,以這個注冊時間為基準,至少1000*i ms(這里的i肯定0 1 2 3 4 而不一直都是5,因為我注冊事件的時候就要用到i,那就要拿到當時的i值)之后去執行這個fn(setTimeout里面的fn回調函數);
那么五次循環之后(阻塞隊列執行完畢),實際上就是已經注冊了五個不同時間節點的非阻塞事件,雖然說可以說五個非阻塞事件注冊時間基本上是同一個時間點,但是程序執行的確是要花費時間的,只能說太快,事件太少,所以我們基本可以理解為這個5個setTimeout注冊幾乎就在一個房間,而每次注冊的時候已經告訴了這個回調函數至少多少ms之后執行(就是告訴ABC...要相對這個房間走多少步)。
所以,他們的確是針對注冊這個非阻塞事件時 , 0s 5 ;1s 5 ;2s 5 ;3s 5 ;4s 5 ;
為什么用立即執行函數(IIFE)就可以解決, 以及ES6中的let
因為閉包
for(var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
},1000*i);
})(i)
}
實際上就是每個非阻塞事件再執行的時候去查找變量,通過IIFE給setTimeout與for循環之間多加了一層向上查找變量的作用于,本來是
其實你可以試試 把加入 IIFE 的console.log( j )變成console.log( i );
結果就還是 ** 5 個 5 因為 IIFE提供的閉包環境中 只提供了一個 j 變量, 當沒有找到 i 時 ,又得向上查找,這個時候就找到了阻塞事件**執行完畢之后的 i ,那還不就 5 嘛~
let 其實就是讓每個for循環有自己作用域塊,從** 5 個 console.log(i)對應一個 ** i (不加let)變成 5 個 console.log(i)對應5個 i ,這每個 i 并不是同一個了。
結束
寫到這里,其實已經很長了,再寫下去可能就要開始乏味了。那我再說兩個小點吧
- promise : 其實就是 阻塞事件 的 干兒子,不管setTimeout在promise之前多少(當然我們這里說的是同一個任務),每次去注冊非阻塞事件的時候,promise肯定是在阻塞隊列結束之后立即去執行的,就好比 阻塞事件每次執行完了就會說 “干兒子(promise),我好了你趕緊到我后面來,別讓setTimeout它們先跑了”
- setTimeout(fn,times),setInterval(fn,times): 它們中的 times,并不是說 隔 **xxx ms ** 后去執行,而是至少 xxx ms 只執行,為什么呢,說太多寫不下,自己慢慢體會。