JavaScript 函數的執行時機

The time is out of joint. O cursèd spite, that ever I was born to set it right!javascript

—— Hemlet Act1, Scene 5, 186-190html

從一段代碼講起

😉 一段十分普通的JS代碼

  • 先來看一段十分普通的JavaScript代碼,咱們試圖在控制檯用循環語句輸出幾個數字。
let i = 0
for(i = 0; i<6; i++){
  console.log(i)
}
複製代碼
  • 咱們在Chrome和Firefox中分別運行了一下,運行結果以下:

Chrome運行結果 java

屏幕快照 2020-03-06 上午10.41.33.png

Firefox運行結果 segmentfault

屏幕快照 2020-03-06 上午10.37.36.png

  • 用腳趾想都知道,結果是會輸出從0到5的6個數。

😏 給代碼加點料

  • 下面咱們給代碼加點料,咱們仍然使用一樣的循環語句輸出數字,只不過將console.log語句放在了setTimeout函數中且設置延時爲0,看看控制檯會輸出什麼結果?
let i = 0
for(i = 0; i<6; i++){
  setTimeout(()=>{
    // 延時函數
    console.log(i)
  },0)
}
複製代碼
  • 運行結果是以下:

Chrome運行結果 瀏覽器

Chrome運行結果.png

Firefox運行結果 閉包

Firefox運行結果.png

  • 能夠看到,兩個瀏覽器的運行結果是相同的,都在控制檯打印出了6個6

🤯 WTF?

什麼鬼?怎麼會這樣呢!併發

先來談談setTimeout函數

😳 setTimeout() 函數是幹嗎的?

  • 查詢mdn能夠看到,文檔中給出的描述是:
  • setTimeout() 方法設置一個定時器,該定時器在定時器到期後執行一個函數或指定的一段代碼。
  • 再去W3School看看,他們給出的描述是:

setTimeout() 方法用於在指定的毫秒數後調用函數或計算表達式異步

  • 聽起來彷佛更容易理解了一些,看看代碼示例也許更容易理解
/* 瀏覽器3秒後向你打個招呼! */
setTimeout(
  function(){
    alert("Hello"); 
  }, 3000);
複製代碼
  • 對於setTimeout()函數,更加白話的理解是:

凡是放在這個函數中東西,都過一會再作,至於過多久,能夠經過設定毫秒數來調節。 **ide

😅 那麼若是將延時設置爲0呢?

  • 用邏輯來理解的話,延時爲0s === 再也不過好久而是「**當即立刻」**就作,但是「當即」究竟有多「當即」呢?「立刻」究竟有多「立刻」呢?
  • 要嘗試理解「當即立刻」,須要引入Event Loop的一些概念。

🤔 當咱們使用setTimeout() 時,到底發生了什麼?

下面一部分的資料基原本自於如下這個視頻,建議你看看這個視頻,講得一級棒!( Ichiban! ) B站地址:什麼是事件循環? - Philip Roberts(視頻審覈中)函數

  • 接下來咱們要看看當使用setTimeout() 時,到底發生了什麼。

setTimeout()究竟作了什麼?

咱們使用一張圖來演示在調用setTimeout()時發生了什麼。不用看圖,先往下看文字。

main01.png

😬 一些術語的簡單解釋

你能夠先跳過這部分,固然,看看也無妨。

  1. 調用棧(Call Stack)
  1. 每當咱們調用一個函數的時候,這個函數就會被添加進調用棧並開始執行
  2. 正在調用棧中執行的函數若是調用了其餘函數,那麼那個函數也會被放入調用棧
  3. 調用棧中的函數執行完了以後,會被清出調用棧
  • 說白了就是:要執行的函數push到調用棧頂部,執行完從調用棧頂部pop出來,後進先出。

關於調用棧的更多信息,能夠參考你不知道的JS錯誤和調用棧常識

  1. 定時器(Timer)
  • 定時器能夠理解爲:定時執行某段代碼,這裏的setTimeout()函數就是JS爲咱們提供的一個定時器。

關於定時器的更多信息,能夠參考JavaScript標準參考教程 - 定時器

  1. Web APIs
  • Web APIs 是瀏覽器建立的一些線程,包含計時器等等。
  1. 回調序列(Callback queue)
  • 一個包含了回調函數的有序序列。

**

  1. 事件循環(Event Loop)的簡單描述
  • 事件循環包含了如下幾個步驟:
    • 函數入棧執行,當執行到定時器(這類異步任務)時,把它丟給Web APIs去執行,接着繼續執行棧內的剩餘任務(同步任務),直到棧空;
    • 在此期間Web APIs會執行定時器,直到計時結束,而後會將回調函數(也就是setTimeout的第一個參數)扔到回調序列中;
    • 當調用棧爲空時,事件循環會把Callback中的一個任務放入棧中,開始執行,回到第一步;

看了這麼多概念後,也許你已經一頭霧水了,不要急,接下來咱們用圖片演示一遍事件循環的過程,以後你再回來看這些定義估計會豁然開朗了。

🧐 圖解setTimeout執行過程

  • 依舊是這段代碼,咱們用圖解的形式理解一下。
let i = 0
for(i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}
複製代碼
  1. 首先咱們定義了變量i,併爲它賦值,主程序開始。

main02.png

  1. 而後進入循環,循環的第一步就是判斷i<6是否成立,須要把判斷i<6的語句放入調用棧中執行。

main03.png

  1. 此時i的值是0,i<6顯然成立,會繼續執行循環體內的代碼,即setTimeout()。

main04.png

  1. 做爲一個定時器,setTimeout()會被扔到Web APIs中執行。

main05.png

  1. 此時,調用棧會繼續執行後續代碼,由於本次循環已經完成,因此會再次判斷i<6,並進入下一次循環。

main06.png

  1. 幾乎是在同一時刻(0s),計時器完成了計時,將回調函數扔到回調序列中。

main07.png

  1. 注意,這時候回調序列中的任務並不會立刻執行,須要等到棧空纔會開始進棧執行,所以會執行繼續主程序。也就是循環體中的setTimeout(),由於是定時器,會被扔到Web APIs中執行。

main08.png

  1. 幾乎是在同一時刻(0s),計時器完成了計時,將回調函數扔到回調序列中。

main09.png

  1. 如此循環往復,直到i的值變爲6時,循環完全結束,主程序也隨之結束。

main10.png

  1. 此時回調序列中的任務仍是進棧執行,打印i的值,而此時i的值爲6,因此在控制檯打印出了一個6。

main11.png

  1. 回調序列中的任務會逐一進棧執行,直至最後一個回調函數console.log() ,連續打印出六個6。

main12.png

  • 接下來用一段完整的動畫演示(使用了Loupe工具):

fgsdfgsfg.gif

  • 至此,咱們基本解釋了爲什麼文章開頭處那段代碼會輸出6個6,而不是0~5了。
  • 你能夠回頭看看前面介紹的概念,估計會豁然開朗。

我偏要輸出「0~5」!

  • 咱們已經解釋了爲什麼那段代碼會輸出6個6,可是若是我偏要用for循環中嵌套setTimeout()的形式輸出0~5呢?

如下代碼僅供參考,再也不解釋,由於我也不知道怎麼解釋!

😄 方案一

  • 在for循環體內聲明i
for(let i = 0; i<6; i++){
  setTimeout(() => {
  	console.log(i)
  }, 0)
}
複製代碼
  • 運行結果

屏幕快照 2020-03-06 下午5.59.18.png

😁 方案二

  • 先聲明函數,在setTimeout()中調用。
let i = 0
function cb() {
  console.log(i)
}
for(i = 0; i<6; i++){
  setTimeout(cb(), 0)
}
複製代碼
  • 運行結果

屏幕快照 2020-03-06 下午5.59.44.png

😊 方案三

  • 使用當即執行函數?
let i = 0
for(i = 0; i<6; i++){
  (function(i){
  	setTimeout(() => {
  		console.log(i)
  	}, 0)
  })(i)
}
複製代碼
  • 運行結果

屏幕快照 2020-03-06 下午6.00.10.png

😆 方案四

for(var i = 1; i <= 5; i++) {
  setTimeout(console.log.bind(console, i), i * 1000);
}
複製代碼

以上幾種方案的本質都是將i限制在循環中每一次建立的函數實例中,不管是藉助閉包,仍是藉助let的塊做用域,以及 Function.proptotype.bind()。

鑑於本人才疏學淺,若有錯誤之處,還望批評指正!

參考資料

瀏覽器事件循環機制 - 追風箏的人er

如何序列化JavaScript中的併發操做:回調,承諾和異步等待 - itclanCoder

關於JS的for循環包裹異步函數的問題 - microkof

JavaScript運行機制詳解:再談Event Loop - 阮一峯

【演講】What the heck is the event loop anyway? - Philip Roberts

【演講】In the loop - Jake Archibald

相關文章
相關標籤/搜索