setTimeout的delay 0?event loop又是什麼?
setTimeout為何delay 0不是立刻執行?本文還有提到關於quene、同步非同步的一些資訊
在研究 setTimeout 的時候,就在觀看parameter的同時看到了這段敘述
delay: optional: 時間,以毫秒為單位;預設值為 0,如果沒有輸入,則該函式並不是立刻被調用,而是採取”馬上、盡快”執行(更精確的說法是下一個事件循環),但有時候會比預期的時間還要晚
那個事件循環到底是什麼?不就是應該是執行下去原本的code嗎?
這邊有一個經典的影片講述到這件事情:
所以呢!藉由這個影片及一些關鍵字的查詢,讓我開始統整一些觀念
此篇為自我的學習路程紀錄,這篇裡面會提到一些關於同步、非同步、quene、event loop等字眼,如有什麼需要矯正的歡迎鞭策小力一點😅
Start
看到這篇影片之後,讓我回想起了同步Synchronous與非同步Asynchronous,還記得上課的時候提到的是Ajax(Asynchronous javascript and XML)。
一般來說JavaScript是single-threaded程式,且所有的程式碼片段都會在stack中執行,這邊就讓我想起stack的記憶儲存方式:
先進先出的概念,也就是在JS中,需要等到上面的執行結束後才能執行下面的,遇到function狀況的時候又複雜一些,以下是一個例子(出自於上面的影片)
function multiply(a, b) {
return a * b
}
function square(n) {
return multiply(n, n)
}
function printSquare(n) {
let squared = square(n)
console.log(squared)
}
printSquare(4)
一開始執行的時候,會是printSquare先執行(在global執行環境),就會在stack最下層,此時會先建立一個執行環境(execution contexts,這篇有詳細的介紹)並宣告squared,之後賦值與要準備console.log(squared),但這樣又遇到了squared這個function,這時又在建立一層執行環境給squared這個function,此時除了最底層的global執行環境外,已經堆疊了function printSquare、function square兩層執行環境。
而這時又遇到了mutiply,也因此出現了三層的執行環境,這時就由mutiply開始動作,遇到了第一個return,結束後function mutiply的執行環境也結束,便往下進行,一直到執行完printSquare的function,console.log(squared)後,一連串的JS才執行完畢
這邊引用一下PJ老師的影片:
這個執行的特性,一旦遇到龐大的function或者是執行環境,就會導致blocking,如果以影片的例子就是直接將http request變成同步執行的狀況
const foo = $.getSync('//foo.com')
const bar = $.getSync('//bar.com')
const qux = $.getSync('//qux.com')
console.log(foo)
console.log(bar)
console.log(qux)
這樣子的狀況,JavaScript會先從上往下執行,所以會先request網頁,這時候會導致render失效,而無法使用,造成blocking,這也會影響使用者體驗。
所以知道了同步執行的狀況,也清楚同步執行的壞處後,就更了解到了非同步執行的好處,而非同步的情況只發生在Web、browser上。
在browser、web的情況,因為多出了rendering engine與http request,所以才可以採取非同步的執行,但JavaScript依然是single-threaded的模式在進行著。
而現階段我所學到的Web的API可以達到非同步處理的狀況的有:addEventListener、setTimeout、Ajax等。
addEventListener要有監聽器,才可以將事件的觸發變成訊息放入task quene中;而setTimeout是一個web API,也是執行後,會將第一個參數視為訊息,
待第二個參數的時間結束後,才會將此訊息放入task quene;Ajax也是請求完資料後,才將訊息放入task quene。
什麼是task quene?
另外一個很關鍵的空間就是task quene,也是event quene;quene一個訊息的佇列,裡頭所存放的都是待處理的訊息,其中每個訊息都與一個function相關聯。
只有當stack中主程式執行完畢後,且都被移除後,event loop才會從quene拿取較舊的訊息放入stack中去執行。
那所以event loop又是什麼?
以下主要根據此篇的內容做解釋
while (queue.waitForMessage()) {
queue.processNextMessage();
}
他就是一個這樣的存在,如果沒有任何quene的訊息,就會一直是等待的狀況。
每一個訊息處理完之後才會執行下一個
所以往下舉例:
console.log("Hi")setTimeout(function cb () {
console.log("there")
}, 5000)console.log("JSConfEU)
並搭配這個圖看一下,
因為現在在web上執行,也清楚setTimeout是web提供給我們的計時器的工具,所以在執行主要程式碼的時後,會先進行第一個執行console.log(“Hi”),就來就進行setTimeout這個function。
對於這個setTimeout來說,javascript的任務就是將他丟進web API中,讓web API幫我們計時,那他的任務就結束了,接著就是往下一步走console.log(“JSConfEU”),印出之後主程式也跟著結束了。
而這時web API幫我們倒數後的function cb會因為時間結束而預備被丟出,但我們不可能隨時去接受web api這樣傳入資料的方式,否則會很紊亂,所以web api會把“訊息”丟到task quene,而這時event loop的第一行程式碼:while (queue.waitForMessage())就是true,而將第一個任務丟進javascript中,並開始此程式的執行。
所以setTimeout delay = 0?
這邊就很好解釋了,而且講者也有舉例:
console.log('hi')
setTimeout(function () {
console.log('there')
}, 0)
console.log('JSConfEU')
雖然在文中,有說明delay = 0表示「立即、馬上」的意思,但實際上,仍然會有延遲,主因就是因為setTimeout只是處理將該function的計時功能丟入web api中,儘管他直接被web api處理好訊息而被丟到task quene中,仍需要等待後面console.log(“JSConfEU”)執行及主程式執行完畢後,event loop才會啟動,也因此有了些微delay的狀況,在這邊文中也提到這個delay時間,是一個最小時間,而非保證時間。
以下影片支援:
值得一提的是…
在這個影片裡得知到了一個新東西,也就是web渲染的機制優先於javascript 函式執行的機制,也因此更明白了非同步的優點。
在JS中,每一次執行一個程式都會使渲染停住,一旦執行大的程式就會使卡住的狀況越顯嚴重,這也相當的影響使用者體驗,也跟上面所提到直接將http request變成同步的概念是一樣的,這時候使用者會什麼事情都不能做,因為畫面沒有辦法渲染,也因此使用ajax去request,會因為ajax這個程式已經在js中被執行完畢(既將request丟到web去)而結束,使得後面的渲染動作與function執行可以順利完成,等到stack完全結束後,再將request的資料由event loop從task quene取出,再放入Javascript中!
影片支援:
綠色與紅色的數字,可以很明顯的看得出來什麼時候暫停,什麼時候繼續!
End
總結來說,真的是認識了這個event loop的機制,也認識了很多觀念,這讓我對於使用者體驗的好壞有了新的認識,因為不只是setTimeout的機制,addEventListener、ajax及其他API的使用都有很大關連,還記得之前曾經將一個function內放入addEventListener的機制,當初也不覺得有什麼,然而現在想想就更清楚一些,對於這樣的綁定方式是好是壞?對使用者體驗會不會有什麼不預期的狀況發生?都比較能夠提出問題並且去驗證看看!真的是很有收穫!
以下是有參考的網頁及資訊:
https://pjchender.blogspot.com/2017/08/javascript-learn-event-loop-stack-queue.html