[譯]深刻理解JavaScript函數執行—調用棧,事件循環和任務等

Web 開發者,或者前端工程師(咱們更喜歡別人這麼稱呼)現現在幾乎能作全部的工做,從扮演一個瀏覽器內部交互性的角色,到製做電腦遊戲、桌面控件、跨平臺手機應用,甚至還能夠把它寫在服務器端(最流行的是node.js)和數據庫鏈接——做爲一個腳本語言,實現卻近似無所不在。所以弄明白JavaScript的內部機制很是重要,這有助於咱們更好地和更有效率的使用它,而這些就是本篇文章要講的內容。javascript

如今JavaScript生態變得比以往都要複雜,並且將來還會更加複雜。構建一個現代web應用可以使用的工具備WebPack、Babel、ESLint、 Mocha、 Karma、 Grunt等等——這些工具我該用哪一個呢,每一個工具又是用來幹嗎的呢?。我發現這個web漫畫,生動的詮釋了今天web開發者的心裏的糾結。 前端

JavaScript疲勞——學習JavaScript的感受

在每一個JavaScript開發者在一頭扎進框架或者庫的使用以前,首先須要作的就是知曉 如何在最根本的層面上實現全部這些的基礎。幾乎全部的JS開發者都據說過術語 「V8」 、Chrome的運行時,但一些人可能並不真的懂得他們的意義以及用處。最初我在從事開發工做的第一個年頭,對這些花哨的術語也不太瞭解,由於更多的是先完成工做。而這並不能知足我對JavaScript是如何作到這些事情的好奇心。我決定深挖,查遍谷歌,而後發現好的博客文章不多。而這很少的有用信息中就包括一位Philip Roberts的大牛,及其視頻到底什麼是Event Loop呢? | 歐洲 JSConf 2014。所以我決定總結我在視頻中所學到的並把它分享出來。由於有不少事情須要先作解釋,我就把文章分爲2個部分。本部分將介紹用到的術語,第二部分再把他們給串起來。java

JavaScript是一門單線程單併發語言,意味着它一次只能處理一個任務,或者一次只運行一條代碼。它有一個單獨的調用棧(call stack),與堆、隊列等其餘部分一塊兒構成Javascript併發模型(在V8中實現)。咱們首先簡要介紹下每一個術語: node

JS模型的可視化表示

  1. 調用棧(Call Stack):調用棧是一個記錄函數調用的數據結構。若是咱們調用一個函數去執行,咱們就會在這個棧中push東西。當咱們從一個函數返回時,棧頂就pop出該函數。
    JS棧可視化模型

當運行程序時,咱們首先查找main函數——咱們全部其餘的函數都是在main函數中執行。如上面GIF圖所示,運行首先開始於 console.log(bar(6)),所以其被push到棧中。下一幀是函數bar和他的參數,而bar調用函數foo,所以foo也被push到棧中。web

foo 當即執行完成後返回,所以從棧頂彈出。相似的bar也從棧中彈出,最後是console彈出並打印輸出。全部這些都發生在毫秒級的時間裏。ajax

我想大家必定見過瀏覽器控制檯中有時會出現的紅色錯誤堆棧跟蹤,它基本上指示了調用棧的當前狀態,而函數報錯的從頂到底的方式和棧同樣。(見下圖) 數據庫

錯誤棧追蹤

有時候,在咱們調用遞歸函數的時候會進入一個無線循環的狀況,而Chrome瀏覽器限制棧的大小是16000幀,若是超出就會終止掉你的進程並彈出Max Stack Error Reached(見下圖) 瀏覽器

2. 堆(Heap):對象在堆中分配,即堆中的大部分是非結構化的內存區域。變量和對象的內存分配都發生在這裏。

  1. 隊列(Queue ):一個js 運行時包含一個消息隊列,它是一個要處理的消息和相關要調用的函數的的列表。當棧有足夠的容量時,從隊列中取出消息並進行處理,該消息包括調用關聯函數(從而建立初始堆棧幀)。當消息處理結束時,棧又變成了空的。簡言之,這些消息是根據外部的異步事件(例如鼠標被單擊或接收對HTTP請求的響應)排隊的,由於已經提供了回調函數。若是,好比有人點擊一個按鈕,而按鈕沒有提供回調函數,就不會有消息去排隊。

事件循環

總的說來,當咱們評估js代碼性能時,是棧中的函數來決定是快仍是慢,console.log()運行很快,而執行for或者while進行大量迭代的函數則會慢得多,而且在執行時會保持堆棧被佔用或阻塞。這就是大家在Webpage Speed Insights上聽到或看到的術語:阻塞腳本。ruby

網絡請求可能會很慢,圖片請求可能會很慢,但謝天謝地,服務器請求能夠經過異步的AJAX完成。試想,假如這些網絡請求是經過同步功能實現的,將會發生什麼?。網絡請求被髮送到一些服務器上,它通常是另外一臺計算機/機器。如今,計算機能夠很慢地回送響應。同時,若是單擊某個按鈕,或者須要執行其餘渲染,當棧被阻塞時,就什麼也作不了。在多線程語言像ruby,別的請求能夠被處理。但在單線程語言像js,在棧中函數return一個值以前,別的請求想要被處理就顯得不太現實。在瀏覽器不能作任何事時,網頁就糟糕透頂。若是咱們想要用戶的體驗流暢的UI,這是很是不理想的。那麼,咱們該怎麼解決呢?服務器

「Concurrency in JS— One Thing at a Time, except not Really, Async Callbacks」

最簡單的方式就是使用異步回調,異步回調意味着咱們運行代碼的一部分,而後給它一個、在後面執行的回調函數。咱們必定都遇到過異步回調像$.get()這樣的ajax請求、setTimeout()、setInterval()、Promise等等。Node中所有都是關於異步函數執行的。全部的這些異步回調都是不當即運行,而是在某個時間以後才運行,所以他們不會像console.log(), 算數運算等這些同步函數同樣被當即入棧。那麼,他們到底去哪裏了,又該怎麼處理?
若是咱們在JavaScript中看到一個相似於上面代碼的網絡請求:

  1. 執行請求函數,在onreadystatechange事件中傳遞匿名函數做爲回調,以便在未來某個時候響應可用時執行。
  2. console會當即輸出「Script call done!」。
  3. 未來的某個時間,響應到來而且咱們的回調執行,輸出他的響應body到console 調用者與響應的解耦容許JavaScript runtime 在等待異步操做完成及其回調觸發時執行其餘操做。

2這是瀏覽器自身的API發揮做用的地方,調用這些API處理諸如DOM事件、HTTP請求、StimeTimeUT等異步事件。(知道了這一點以後,在Angular 2 中,使用Zones來對這些API進行從新封裝,以引發運行時更改檢測,我如今能夠了解一下它們是如何實現的)

如今,這些 WebAPI 自己不能將執行代碼放到堆棧中,若是放的話,那麼這些執行代碼將隨機出如今大家代碼中。上面討論的消息調用隊列闡釋了這一過程。3WebAPI中的任何一個在執行完後將回調推送到隊列中。事件循環如今負責在隊列中執行這些回調,並當堆棧爲空時,將其推送到堆棧中。4事件循環的基本工做就是盯着棧和任務隊列,當看到棧爲空時,將隊列中的第一項推到堆棧。在處理任何其餘消息以前,會徹底處理每一個消息或回調。

在Web瀏覽器中,任何事件發生時都會添加消息,而且附加了事件偵聽器。若是沒有偵聽器,則事件丟失。所以,單擊一個帶有單擊事件處理程序的元素,將添加一個消息,而任何其餘事件也是如此。這個回調函數的調用充當調用堆棧中的初始幀,而且因爲JavaScript是單線程的,因此在堆棧上返回全部調用以前,將暫停進一步的消息輪詢和處理。後續(同步)函數調用將新的調用幀添加到堆棧中。

在下一部分,我將展現上述過程的代碼執行的可視化動畫,進一步解釋什麼是不一樣類型的異步函數,如任務、微任務以及隊列中誰的優先級高等。此外,相似零延遲的黑客用來執行某些功能。

但願,各位讀者喜歡。您的寶貴意見,就是對我最大的支持。

註釋

文章原文地址: Understanding Javascript Function Executions — Call Stack, Event Loop , Tasks & more

相關文章
相關標籤/搜索