Web 開發者,或者前端工程師(我們更喜歡別人這麼稱呼)現如今幾乎能做所有的工作,從扮演一個瀏覽器內部互動性的角色,到製作電腦遊戲、桌面控制元件、跨平臺手機應用,甚至還可以把它寫在伺服器端(最流行的是node.js)和資料庫連線——作為一個指令碼語言,實現卻近似無所不在。因此弄明白JavaScript的內部機制非常重要,這有助於我們更好地和更有效率的使用它,而這些就是本篇文章要講的內容。
現在JavaScript生態變得比以往都要複雜,而且未來還會更加複雜。構建一個現代web應用可使用的工具有WebPack、Babel、ESLint、 Mocha、 Karma、 Grunt等等——這些工具我該用哪個呢,每個工具又是用來幹嘛的呢?。我發現這個web漫畫,生動的詮釋了今天web開發者的內心的糾結。
在每個JavaScript開發者在一頭扎進框架或者庫的使用之前,首先需要做的就是知曉 如何在最根本的層面上實現所有這些的基礎。幾乎所有的JS開發者都聽說過術語 “V8” 、Chrome的執行時,但一些人可能並不真的懂得他們的意義以及用處。最初我在從事開發工作的第一個年頭,對這些花哨的術語也不太瞭解,因為更多的是先完成工作。而這並不能滿足我對JavaScript是如何做到這些事情的好奇心。我決定深挖,查遍谷歌,然後發現好的部落格文章很少。而這不多的有用資訊中就包括一位Philip Roberts的大牛,及其視訊到底什麼是Event Loop呢? | 歐洲 JSConf 2014。因此我決定總結我在視訊中所學到的並把它分享出來。因為有很多事情需要先做解釋,我就把文章分為2個部分。本部分將介紹用到的術語,第二部分再把他們給串起來。
JavaScript是一門單執行緒單併發語言,意味著它一次只能處理一個任務,或者一次只執行一條程式碼。它有一個單獨的呼叫棧(call stack),與堆、佇列等其他部分一起構成Javascript併發模型(在V8中實現)。我們首先簡要介紹下每個術語:
- 呼叫棧(Call Stack):呼叫棧是一個記錄函式呼叫的資料結構。如果我們呼叫一個函式去執行,我們就會在這個棧中push東西。當我們從一個函式返回時,棧頂就pop出該函式。
當執行程式時,我們首先查詢main函式——我們所有其他的函式都是在main函式中執行。如上面GIF圖所示,執行首先開始於 console.log(bar(6)),因此其被push到棧中。下一幀是函式bar和他的引數,而bar呼叫函式foo,因此foo也被push到棧中。
foo 立即執行完成後返回,因此從棧頂彈出。類似的bar也從棧中彈出,最後是console彈出並列印輸出。所有這些都發生在毫秒級的時間裡。
我想你們一定見過瀏覽器控制檯中有時會出現的紅色錯誤堆疊跟蹤,它基本上指示了呼叫棧的當前狀態,而函式報錯的從頂到底的方式和棧一樣。(見下圖)
有時候,在我們呼叫遞迴函式的時候會進入一個無線迴圈的情況,而Chrome瀏覽器限制棧的大小是16000幀,如果超出就會終止掉你的程式並彈出Max Stack Error Reached(見下圖)
2. 堆(Heap):物件在堆中分配,即堆中的大部分是非結構化的記憶體區域。變數和物件的記憶體分配都發生在這裡。- 佇列(Queue ):一個js 執行時包含一個訊息佇列,它是一個要處理的訊息和相關要呼叫的函式的的列表。當棧有足夠的容量時,從佇列中取出訊息並進行處理,該訊息包括呼叫關聯函式(從而建立初始堆疊幀)。當訊息處理結束時,棧又變成了空的。簡言之,這些訊息是根據外部的非同步事件(例如滑鼠被單擊或接收對HTTP請求的響應)排隊的,因為已經提供了回撥函式。如果,比如有人點選一個按鈕,而按鈕沒有提供回撥函式,就不會有訊息去排隊。
事件迴圈
總的說來,當我們評估js程式碼效能時,是棧中的函式來決定是快還是慢,console.log()執行很快,而執行for或者while進行大量迭代的函式則會慢得多,並且在執行時會保持堆疊被佔用或阻塞。這就是你們在Webpage Speed Insights上聽到或看到的術語:阻塞指令碼。
網路請求可能會很慢,圖片請求可能會很慢,但謝天謝地,伺服器請求可以通過非同步的AJAX完成。試想,假如這些網路請求是通過同步功能實現的,將會發生什麼?。網路請求被髮送到一些伺服器上,它一般是另一臺計算機/機器。現在,計算機可以很慢地回送響應。同時,如果單擊某個按鈕,或者需要執行其他渲染,當棧被阻塞時,就什麼也做不了。在多執行緒語言像ruby,別的請求可以被處理。但在單執行緒語言像js,在棧中函式return一個值之前,別的請求想要被處理就顯得不太現實。在瀏覽器不能做任何事時,網頁就糟糕透頂。如果我們想要使用者的體驗流暢的UI,這是非常不理想的。那麼,我們該怎麼解決呢?
最簡單的方式就是使用非同步回撥,非同步回撥意味著我們執行程式碼的一部分,然後給它一個、在後面執行的回撥函式。我們一定都遇到過非同步回撥像$.get()這樣的ajax請求、setTimeout()、setInterval()、Promise等等。Node中全部都是關於非同步函式執行的。所有的這些非同步回撥都是不立即執行,而是在某個時間之後才執行,因此他們不會像console.log(), 算數運算等這些同步函式一樣被立即入棧。那麼,他們到底去哪裡了,又該怎麼處理? 如果我們在JavaScript中看到一個類似於上面程式碼的網路請求:“Concurrency in JS— One Thing at a Time, except not Really, Async Callbacks”
- 執行請求函式,在onreadystatechange事件中傳遞匿名函式作為回撥,以便在將來某個時候響應可用時執行。
- console會立即輸出“Script call done!”。
- 將來的某個時間,響應到來並且我們的回撥執行,輸出他的響應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