譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述

ziling_dzl發表於2019-03-21

原作者:blog.sessionstack.com/@zlatkov

隨著JS變得越來越流行,開發團隊們在多個級別的堆疊中都借力於js的支援- 前端,後臺,混合式應用開發,嵌入式裝置等等。

這篇博文旨是我們深入挖掘JavaScript和其工作原理的系列文章的第一篇:我們認為通過了解JavaScript的構建塊和他們是如何共同發揮作用的,你能寫出更好的程式碼和應用。我們也會分享我們在構建的一個輕量級的但必須具有強大和高效能的特點才能保持競爭力的JavaScript應用SessionStack時的一些經驗法則。

正如GitHut Stats上的資料所示,JavaScript在github上的包活躍度和推送量居於領先地位。它也沒有落後於其他類別的庫。

譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述

檢視github上最新的統計資訊

如果專案依賴JavaScript越來越多,則意味著開發者不得不用對其內部原理越來越深的理解來利用好語言和其生態系統所提供的一切以構建令人驚奇的軟體。

事實證明,雖然有大量的開發者每天都會使用JavaScript,但是他們並不知道引擎蓋下發生了什麼。

概述 

幾乎人人都已經聽說過V8引擎這個概念,大多數人也知道JavaScript是單執行緒或使用回撥佇列。

在本文中,我們將會詳細地瞭解這些概念和解釋JavaScript實際上是怎樣執行的。知道了這些細節後,你講能夠寫出更好的、正確使用了提供的api的非阻塞的應用。

如果你對JavaScript這麼語言還比較陌生,那麼本文可以幫助你理解為甚JavaScript和其他語言相比這麼“神奇”。

如果你已經是一名富有經驗的JavaScript開發者,那我們希望本文能在你每天都在使用的JavaScript執行時到底如何工作的問題上提供一些新的見解。

JavaScript 引擎

JavaScript引擎的一個流行例項是谷歌的V8引擎,V8引擎運用在谷歌瀏覽器和node.js內部。這裡有一個簡化的檢視:


譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述

引擎由主要由兩部分組成:

* 記憶體堆—這裡是記憶體分配發生的地方。

* 呼叫堆疊—這是當你的程式碼執行時堆疊幀的位置

執行時

瀏覽器中有許多api幾乎每個JavaScript開發者都使用過(比如“setTimeout”),然而,這些api並不是引擎提供的。

那麼,他們是哪兒來的呢?

事實上,情況有些複雜。

譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述

所以,我們不僅有引擎,還有更多東西。我們有瀏覽器提供的Web APIs,比如,DOM, AJAX, setTimeout 等等。

此外,還有如此受歡迎的事件迴圈和回撥佇列。

呼叫堆疊(The Call Stack)

JavaScript是一門單執行緒的程式語言,這意味著它只有一個堆疊(Call Stack),一次只能做一件事情。

堆疊是一種用來記錄程式碼在程式中的位置的資料結構,當我們執行到一個函式呼叫時,我們把它放在堆疊的頂部,當函式執行完以後,我們把它從堆疊的頂部移除(出棧)。這就是堆疊所能做的全部了。

讓我們來看個例子,看看下面的程式碼:

譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述

當引擎開始執行程式碼的時候,堆疊裡面是空的。然後,會按下面這幾個步驟執行:

譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述

每一次進棧被稱為一個棧幀(Stack Frame)。

這正是丟擲異常時堆疊跟蹤的構造方式 — 它基本上是異常發生時呼叫堆疊的狀態。看看下面的程式碼:

譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述

如果在谷歌瀏覽器中執行(假如是在一個叫foo.js的檔案裡),將會發生以下的堆疊跟蹤:

譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述

“堆疊溢位(Blowing the stack)”—當達到堆疊呼叫最大容量時會發生這種情況。要發生這種情況也很簡單,尤其是當你使用遞迴而沒有廣泛地測試你的程式碼的時候。看看下面的例子:

function foo(){
    foo()
};

foo();
複製程式碼


當引擎開始執行這段程式碼的時候,它開始呼叫‘foo’函式,然而‘foo’是一個遞迴它以呼叫它本身開始,並且沒有任何終止條件。所以,執行的每一步,這個函式會一遍又一遍地進棧。看起來就像這樣:

譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述

但到了一定程度,函式呼叫的數量超出了堆疊本身的容量,瀏覽器就會採取行動,丟擲一個錯誤。類似這樣:


                    譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述

在單執行緒中執行程式碼會非常簡單,因為你不用處理一些發生在多執行緒環境裡的複雜場景。比如:死鎖(deadlocks)

但是在單執行緒中執行也有很多限制,由於JavaScript只有一個Call Stack,那麼當事情變慢時會發生什麼?

併發和事件迴圈

如果在呼叫堆疊中有函式呼叫需要花費大量時間才能處理,會發生什麼?比如,想象一下你想用JavaScript在瀏覽器上做一些複雜的圖片轉換。

你或許會問—為什麼這是一個問題?問題就是當堆疊裡有函式需要執行的時候,瀏覽器實際上任何其他事都做不了—它被阻塞了。這就意味著瀏覽器不能渲染,不能執行任何其他程式碼,只能卡住了。如果你想要在你的應用中有流暢的UI,這就是個問題了。

這還不僅僅是唯一的問題。一旦瀏覽器要在堆疊中處理很多工的時候,可能會很長一段時間沒有響應。這時候大多數瀏覽器都會丟擲錯誤,詢問你是否要終止該網頁。

                  譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述

現在,這不是最好的使用者體驗,對嗎?

所以,我們如何既能執行繁重的程式碼同時也不會阻塞UI渲染或導致瀏覽器不響應呢?well,解決辦法就是非同步回撥。

這在“JavaScript如何工作”的第二部分教程“V8引擎內部+關於如何編寫優化程式碼的5個技巧”(我的譯文)中有更詳細的解釋。

同時,如果你很難再現和理解你的JavaScript應用中出現的問題,你可以看看SessionStack.

SessionStack記錄了你的應用中發生的任何事情:所有DOM更改,使用者互動,JavaScript異常,堆疊跟蹤,失敗的網路請求和除錯訊息。

使用SessionStack,你可以將網路應用中的問題作為視訊重播,並檢視使用者發生的所有事情。

這兒有一個免費的方案,不需要任何的通行證。Get Start Now

譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述



相關文章