[譯]JavaScript的調用棧、回調隊列和事件循環

譯者按
這篇文章能夠看作是對Philip Roberts 2014年在JSConf演講的 《What the heck is the event loop anyway?》的一個總結。
建議先看Philip Roberts的這個演講而後再閱讀本篇文章。這哥們兒的演講語言幽默風趣,內容通俗易懂,很是值得一看。

在這個視頻中,Philip Roberts將JavaScript的調用棧、回調隊列和事件循環的內容講的很清晰。因此你能夠隨意的跳過這篇文章,花上一個半小時去看視頻。固然若是你願意讀一下個人這篇文章那也不是不能夠。ajax

什麼是JavaScript

什麼是JavaScript呢?列舉一些關鍵詞就是:api

  • 他是單線程的、非阻塞的、異步的併發語言
  • 他有一個調用棧,一個事件循環,一個回調隊列,還有一些api和別的東西

若是你像我同樣(或者像Philip Roberts)對此懵逼的話,這些話自己並沒不意味着什麼。那咱們就來剖析一下。瀏覽器

JavaScript運行時

JavaScript運行時(像V8引擎)擁有一個堆(內存分配用的)和棧(執行上下文)。可是他沒有setTimeoutDOM等。這些是瀏覽器提供的Web APIs網絡

咱們瞭解的JavaScript

瀏覽器中的JavaScript擁有:數據結構

  • 一個像V8引擎同樣的運行時(提供堆棧)
  • 瀏覽器提供的Web APIs,例如:DOMAJAXsetTimeout
  • 一個爲各類事件回調準備的回調隊列,例如:onClickonLoadonDone
  • 一個事件循環

clipboard.png

什麼是調用棧

JavaScript是單線程的,意味着他有一個單獨的調用棧,意味着他一次能作一件事。調用棧基本上就是一個記錄程序執行位置的數據結構。若是程序進入了一個函數,那就往這個棧裏面塞些東西。若是程序從一個函數中return了,那就從棧頂彈出一些東西。併發

當咱們的程序報錯的時候,咱們會在控制檯看到調用棧信息。報錯的時候咱們能夠看到棧的狀態(被調用的那個函數的)。異步

阻塞

這涉及到一個重要的問題:程序運行的很慢的時候發生了什麼?換句話說,就是程序阻塞了。阻塞並無嚴格的定義。實際上就是程序執行慢。執行console.log不慢,可是一個從1到1,000,000,000的while循環,圖像處理或者網絡請求這些操做的執行就比較費時了。這些執行慢的東西堆在一塊兒就發生了阻塞。函數

由於JavaScript是單線程的,咱們發起一個網絡請求就不得不一直等到他結束。這在瀏覽器中就是個問題--當咱們等這個請求的時候,瀏覽器就發生了阻塞(咱們不能作點擊、提交表單等操做)。解決這個問題的方法就是使用異步回調。工具

併發,看到這個詞的時候咱們會發現上面有一個地方說的不對

JavaScript一次只能作一件事情的說法是不對的。正確的說法應該是:JavaScript的運行時一次只能作一件事。他不能一邊發ajax請求一邊運行別的代碼,也不能在執行別的代碼時候運行一個定時器。可是咱們能夠併發的作這些事。由於瀏覽器不單單是一個運行時(還記得上面那個渣渣畫質的圖嗎?)。oop

調用棧能夠往Web APIs裏面放東西,Web APIs能夠在事件結束的時候把回調函數放進回調隊列,而後是事件循環。最終咱們進入事件循環,這是這個過程當中最簡單的部分,他有一個很是簡單的工做:看看調用棧,瞅瞅回調隊列,若是調用棧空閒了,就把回調隊列中的第一個函數取出來丟進調用棧讓他執行(這就回到了JavaScript的地盤,回到了V8的內部)。

整個串起來

Philip搞了一個的碉堡的工具來可視化這個過程,這玩意兒叫Loupe。這是一個可以把JavaScript運行時可視化的工具。

咱們用它來看一個簡單的例子:在一個異步的setTimeout回調中用console.log在控制檯打些log出來。

clipboard.png

整個過程到底都發生了什麼呢?咱們來看一下:

  1. 執行進入console.log('Hi');函數,所以這個函數被丟進了調用棧裏。
  2. console.log('Hi');函數return了,所以他就被彈出了棧頂。
  3. 執行進入setTimeout函數,所以這個函數被丟進了調用棧裏。
  4. setTimeoutWeb APIs的一部分,所以Web APIs處理了他,而且等了2秒
  5. 繼續執行腳本,進入console.log('EvenyBody')函數,把他也丟進調用棧。
  6. console.log('EvenyBody')函數return了,因此把他從棧頂彈出去
  7. 2秒的定時已經完成了,因此就把對應的回調函數放到回調隊列裏。
  8. 事件循環檢查調用棧是否爲空,若是非空的話,他就等着。由於調用棧如今是空的,因此把回調隊列中的回調函數丟進調用棧。
  9. console.log('There')函數返回了,所以把他從棧頂彈出去(譯者按:原文爲console.log('Everybody'),應爲書寫錯誤)。

有趣的一點是:setTimeout(function(...), 0)的狀況。setTimeout爲0的時候這個過程看起來可能不明顯,除非考慮到調用棧的執行環境和事件循環的狀況。基本上都會推遲到調用棧爲空才執行。

考慮UI渲染的性能的狀況

爲了回到了咱們平常處理的UI層,咱們須要考慮渲染問題。瀏覽器受到咱們在JavaScript中所作操做的影響,他可能每隔16.6ms重繪一次屏幕(60幀/秒)。可是調用棧還有代碼在執行的話,他其實是無法作重繪的。

就像Philip說的同樣:

當你們說不要"阻塞事件循環"的時候,他們其實是說:不要把耗費時間長的代碼放進調用棧,由於你要這麼搞的話,瀏覽器就不能作他該作的事了,好比說給你搞一個漂亮流暢的UI。

Philip Roberts 「What the Heck Is the Event Loop Anyway」

舉個例子,滾動的處理函數觸發多了會讓UI變得卡頓。順便說一句,這是我聽過的對防抖最清楚的解釋了,這就是你要作到的「不要阻塞事件循環」(那就是咱們只在滾動處理函數被觸發x次後才執行那些耗時的操做)。

結語

總之,這就是《What the heck is the event loop anyway?》的答案。Philip的演講很好的幫我理解了什麼是JavaScript,什麼不是,哪一個部分是運行時,哪一個部分是瀏覽器的和咱們該怎樣有效的使用事件循環。好好看看這個視頻吧。

相關文章
相關標籤/搜索