JavaScript的呼叫棧、回撥佇列和事件迴圈
譯者按
這篇文章可以看做是對Philip Roberts 2014年在JSConf演講的 《What the heck is the event loop anyway?》 的一個總結。
建議先看Philip Roberts的這個演講然後再閱讀本篇文章。這哥們兒的演講語言幽默風趣,內容通俗易懂,非常值得一看。
在這個視訊中,Philip Roberts將JavaScript的呼叫棧、回撥佇列和事件迴圈的內容講的很清晰。所以你可以隨意的跳過這篇文章,花上一個半小時去看視訊。當然如果你願意讀一下我的這篇文章那也不是不可以。
什麼是JavaScript
什麼是JavaScript呢?列舉一些關鍵詞就是:
- 他是單執行緒的、非阻塞的、非同步的併發語言
- 他有一個呼叫棧,一個事件迴圈,一個回撥佇列,還有一些api和別的東西
如果你像我一樣(或者像Philip Roberts)對此懵逼的話,這些話本身並沒不意味著什麼。那我們就來剖析一下。
JavaScript執行時
JavaScript執行時(像V8引擎)擁有一個堆(記憶體分配用的)和棧(執行上下文)。但是他沒有 setTimeout
、 DOM
等。這些是瀏覽器提供的 Web APIs
。
我們瞭解的JavaScript
瀏覽器中的JavaScript擁有:
- 一個像V8引擎一樣的執行時(提供堆疊)
- 瀏覽器提供的Web APIs,例如:
DOM
、AJAX
和setTimeout
- 一個為各種事件回調準備的回撥佇列,例如:
onClick
、onLoad
、onDone
- 一個事件迴圈
什麼是呼叫棧
JavaScript是單執行緒的,意味著他有一個單獨的呼叫棧,意味著他一次能做一件事。呼叫棧基本上就是一個記錄程式執行位置的資料結構。如果程式進入了一個函式,那就往這個棧裡面塞些東西。如果程式從一個函式中return了,那就從棧頂彈出一些東西。
當我們的程式報錯的時候,我們會在控制檯看到呼叫棧資訊。報錯的時候我們可以看到棧的狀態(被呼叫的那個函式的)。
阻塞
這涉及到一個重要的問題:程式執行的很慢的時候發生了什麼?換句話說,就是程式阻塞了。阻塞並沒有嚴格的定義。實際上就是程式執行慢。執行 console.log
不慢,但是一個從1到1,000,000,000的while迴圈,影象處理或者網路請求這些操作的執行就比較費時了。這些執行慢的東西堆在一起就發生了阻塞。
因為JavaScript是單執行緒的,我們發起一個網路請求就不得不一直等到他結束。這在瀏覽器中就是個問題--當我們等這個請求的時候,瀏覽器就發生了阻塞(我們不能做點選、提交表單等操作)。解決這個問題的方法就是使用非同步回撥。
併發,看到這個詞的時候我們會發現上面有一個地方說的不對
JavaScript一次只能做一件事情的說法是不對的。正確的說法應該是:JavaScript的執行時一次只能做一件事。他不能一邊發ajax請求一邊執行別的程式碼,也不能在執行別的程式碼時候執行一個定時器。但是我們可以併發的做這些事。因為瀏覽器不僅僅是一個執行時(還記得上面那個渣渣畫質的圖嗎?)。
呼叫棧可以往Web APIs裡面放東西,Web APIs可以在事件結束的時候把回撥函式放進回撥佇列,然後是事件迴圈。最終我們進入事件迴圈,這是這個過程中最簡單的部分,他有一個非常簡單的工作:看看呼叫棧,瞅瞅回撥佇列,如果呼叫棧空閒了,就把回撥佇列中的第一個函式取出來丟進呼叫棧讓他執行(這就回到了JavaScript的地盤,回到了V8的內部)。
整個串起來
Philip搞了一個的碉堡的工具來視覺化這個過程,這玩意兒叫 Loupe 。這是一個能夠把JavaScript執行時視覺化的工具。
我們用它來看一個簡單的例子:在一個非同步的 setTimeout
回撥中用 console.log
在控制檯打些log出來。
整個過程到底都發生了什麼呢?我們來看一下:
- 執行進入
console.log('Hi');
函式,因此這個函式被丟進了呼叫棧裡。 -
console.log('Hi');
函式return了,因此他就被彈出了棧頂。 - 執行進入
setTimeout
函式,因此這個函式被丟進了呼叫棧裡。 -
setTimeout
是Web APIs
的一部分,因此Web APIs
處理了他,並且等了2秒 - 繼續執行指令碼,進入
console.log('EvenyBody')
函式,把他也丟進呼叫棧。 -
console.log('EvenyBody')
函式return了,所以把他從棧頂彈出去 - 2秒的定時已經完成了,所以就把對應的回撥函式放到回撥佇列裡。
- 事件迴圈檢查呼叫棧是否為空,如果非空的話,他就等著。因為呼叫棧現在是空的,所以把回撥佇列中的回撥函式丟進呼叫棧。
-
console.log('There')
函式返回了,因此把他從棧頂彈出去(譯者按:原文為console.log('Everybody'),應為書寫錯誤)。
有趣的一點是: setTimeout(function(...), 0)
的情況。 setTimeout
為0的時候這個過程看起來可能不明顯,除非考慮到呼叫棧的執行環境和事件迴圈的情況。基本上都會推遲到呼叫棧為空才執行。
考慮UI渲染的效能的情況
為了回到了我們日常處理的UI層,我們需要考慮渲染問題。瀏覽器受到我們在JavaScript中所做操作的影響,他可能每隔16.6ms重繪一次螢幕(60幀/秒)。但是呼叫棧還有程式碼在執行的話,他實際上是沒法做重繪的。
就像Philip說的一樣:
Philip Roberts “What the Heck Is the Event Loop Anyway”
舉個例子,滾動的處理函式觸發多了會讓UI變得卡頓。順便說一句,這是我聽過的對防抖最清楚的解釋了,這就是你要做到的“不要阻塞事件迴圈”(那就是我們只在滾動處理函式被觸發x次後才執行那些耗時的操作)。
結語
總之,這就是《What the heck is the event loop anyway?》的答案。Philip的演講很好的幫我理解了什麼是JavaScript,什麼不是,哪個部分是執行時,哪個部分是瀏覽器的和我們該怎樣有效的使用事件迴圈。好好看看這個視訊吧。