【譯】JS運行時環境

原文地址: The Javascript Runtime Environmentjavascript

原文做者: Jamie Uttariellojava

譯者語:
本文是在學習的過程當中發現的一篇講述JS機制比較明瞭的文章,所以嘗試翻譯了一下。
不是專業的,所以不免有偏頗,歡迎交流指正。
複製代碼

經過本文,咱們一塊兒瞭解一下瀏覽器的JS運行時環境,探究Chrome瀏覽器V8引擎是如何解析代碼,以及事件循環(Event Loop)機制是如何實如今JS單線程中以同步的方式以及某種意義上的異步的方式運行代碼。最後,經過一個常見的例子來更加清楚的解釋一下這一系列過程是如何進行的。算法

回到最初的起點

當你用諸如chrome、火狐、Edge或者Safari等瀏覽器訪問一個wed站點時,事實上每一個瀏覽器都有一個JS運行時環境。瀏覽器對外暴露的供開發者使用的Web API就位於其中。chrome

AJAX、DOM樹、以及其餘的API,都是Javascript的一部分,它們本質上就是瀏覽器提供的、在JS運行時環境中可調用的、擁有一些列屬性和方法的對象瀏覽器

除此以外,用來解析代碼的Javascript引擎也是位於JS運行時環境中的。每個瀏覽器的JS引擎都有本身的版本。Chrome瀏覽器用的是自產的V8引擎,後文中咱們將以它爲例進行分析。bash

V8 JS引擎

當Chrome接收到JS代碼或網頁上的腳本,V8引擎就開始解析工做。首先,它會檢查語法錯誤,若是沒有,按編寫順序解讀代碼最終的目標是將JS代碼轉換成計算機能夠識別的機器語言。可是,在咱們搞清楚JS引擎到底作了什麼來解析代碼以前,咱們首先須要知道解析工做所發生的環境。數據結構

JS運行時環境

咱們能夠把JS的運行時環境看做一個大的容器,裏面有一些其餘的小容器。當JS引擎解析代碼時,就是把代碼片斷分發到不一樣的容器裏。 異步

JS運行時示意圖

運行時環境中的第一個容器就是堆內存,它也是V8引擎的一部分。當V8引擎遇到變量聲明和函數聲明的時候,就把它們存儲在裏面。函數

環境中的第二個容器叫作調用棧, 它也是V8引擎的一部分。當引擎遇到像函數調用之類的可執行單元,就會把它們推入調用棧oop

當函數一被推入執行棧,JS引擎就開始解析函數體,在堆裏建立變量、把新的函數調用推入棧頂、或者把自身分發給WEB API調用所在的第三個容器。

當函數有了返回值,或者被分發到Web API容器,它就會被彈出棧同時下一個函數調用會被推入棧頂。若是JS引擎執行完一個函數,而且該函數沒有明確的指明返回值,JS引擎會默認的返回undefined而後再將之彈出棧。人們一般說的JS同步運行指的就是JS引擎解析函數而後彈出棧(再運行下一個函數)的運行流程。簡言之,在單線程下同一時間只作一件事。

Note:棧是一種LOFO的數據結構 - 後進先出。只有棧頂的函數會被處理,除非前一個函數(處理完畢)被彈出棧,不然JS引擎不會去處理下一個函數。

Web API容器

調用棧內的Web API調用會被分發到Web API容器內,好比事件監聽函數、HTTP/AJAX請求、或者是定時器函數,這些事件會在該容器內直到達到觸發條件。要麼是一個點擊事件被觸發、或者是HTTP請求完成從數據源獲取數據、或者是定時器達到觸發的時間點,一旦達到觸發條件,一個回調函數就會被推入第四個也是最後一個容器: 回調隊列。

回調隊列

回調隊列會按照添加的順序存儲全部的回調函數,而後等待執行棧爲空。當執行棧爲空的時候,回調隊列會把隊列首部的那個回調函數推入執行棧。當執行棧再次爲空的時候,再將此時隊列首部函數推入。

Note: 隊列是一個FIFO的數據結構-先進先出。相比於棧的在尾部添加、移除數據,隊列是在尾部添加數據,在首部移除數據。

事件循環

事件循環能夠被看做是JS運行時環境中的這樣的一個東西:它的工做是持續的檢測調用棧和回調隊列,若是檢測到調用棧爲空,它就會通知回調隊列把隊列中的第一個回調函數推入執行棧。調用棧和執行隊列可能在某一段時間內是閒置的,可是事件循環是永不停歇的檢測前二者的。在任意時間,只要Web API容器中的事件達到觸發條件,就能夠把回調函數添加到回調隊列中去。

這就是人們常說的***JS能夠以異步的方式運行***的含義。這種說法事實上是不正確的,只是看起來像那麼回事兒。JS在同一時間只能執行一個函數,不管在棧頂的是什麼,它是一個同步語言。可是由於Web API模塊能夠不斷的向回調隊列添加回調函數,而回調隊列又能夠不斷的把回調函數函數推入執行棧,咱們能夠認爲JS是在異步運行。這就的是這門語言的強大之處。只擁有同步的能力,卻可以以異步的方式運行,像魔法同樣!

阻塞與非阻塞I/O

當咱們談起阻塞I/O,想象一個函數在被無限循環調用。當函數永不中止的運行時,它就永遠不會被推出棧,所以棧內的下一個函數就會被阻塞的永遠沒法被調用。另外一種狀況是一個有極其複雜的邏輯和算法的函數,它必然會花費大量的時間來執行,那麼就會阻塞下一個函數的執行。上述的會形成阻塞場景是咱們編碼的時候須要知道的,可是相比於語言的的設計缺點,還會有更多的編碼錯誤和糟糕的寫法會形成阻塞。

常見的一個阻塞I/O的操做就是HTTP請求,例如向某個外部網站發送數據請求,你必須等待該網站的迴應。而可能永遠得不到迴應,那麼你的代碼就會被阻塞。好在JS運行時環境中會處理這種狀況。它把HTTP請求分發到Web API模塊,而後把請求操做彈出棧,這樣當請求在Web API模塊內等待響應數據的時候,執行棧內的下一個函數就能夠被執行。即便請求沒法獲得數據,程序的其餘部分也能夠正常執行。這就是咱們所說的JS是一個非阻塞的語言。

一個典型的例子

不少教學視頻和文章都會用相似這樣的例子來解釋JS運行時環境的工做機制:

setTImeout(function(){
    console.log('Hey, Why am I last?')
}, 0)

function sayHi(){
    console.log('Hello')
}

function sayBye(){
    console.log('Goodbye')
}

sayHi()
saybye()
複製代碼

若是你把這段代碼粘貼在控制檯,將會看到先打印出'Hello', 而後是'Goodbye', 而後是undefined,最後是'Hey, why am I last?'. 儘管setTimeout函數最早被調用而且延遲0ms運行,可是它倒是最後輸出的。逐行檢查代碼嘗試理解JS引擎解析代碼的機制。嘗試理解爲何setTimeout函數在sayHi和sayBye函數以後打印輸出。

思考完畢,咱們一塊兒來看看V8JS引擎究竟是怎樣處理這段代碼的...

  1. JS引擎會檢查整段代碼的語法錯誤,若是沒有錯誤,就從頭開始深度解析

  2. 首先遇到setTimeout函數調用,把它推入執行棧頂

  3. 解析函數體,發現setTimeout函數是Web API的一種,所以就把它分發到Web API模塊而後推出棧

  4. 由於定時器設置了0ms延遲,所以Web API模塊當即把它的匿名回調函數推入到回調函數函數隊列。事件循環檢測執行棧是不是空閒,可是當前棧並不空閒,由於...

  5. (6) 當setTimeout函數一被分發到Web API模塊,JS引擎發現了兩個函數聲明,把它們存儲在堆內存裏,而後遇到了sayHi函數的調用,就把它推入了棧頂

  6. 同5同時

  7. sayHi函數調用了console.log函數,所以console.log就被推入了棧頂

  8. JS引擎開始解析console.log的函數體,它接收了一個消息去打印‘Hi’,而後被彈出棧

  9. JS引擎返回到函數sayHi的執行,遇到函數的結束符號}以後,把它彈出棧

  10. sayHi函數一出棧,緊接着sayBye函數被調用,它就被推入棧頂,被解析,調用console.log,把console.log推入棧頂,打印一條消息,彈出棧。而後sayBye函數彈出棧

  11. 同10同時發生

  12. 同10同時發生

  13. 事件循環檢測到執行棧終於空閒了,通知回調隊列,而後回調隊列把其中的匿名函數推入執行棧

  14. 匿名函數(就是setTimeout的回調函數)被解析、調用console.log,console.log推入棧頂

  15. console.log執行完畢、再出棧

  16. 匿名函數再被推出棧,程序結束。

PS:若是你複製代碼在控制檯打印,你會發現有一個undefined被輸出,這是由於程序中全部的主函數都沒有返回值,它們只是調用console.log函數,當log函數被執行並彈出以後,解析器執行至主函數的結尾,並無發現返回值。所以它返回undefined,而後把函數彈出棧。

瀏覽器環境vsNode.js環境

須要注意的時,本文中所討論的環境是瀏覽器下的JS執行環境。雖然Node.js也是用GoogleV8引擎驅動的,可是它提供了一個徹底不同的運行時環境. Node.js 不會提供DOM樹、AJAX、以及其餘的Web API。可是,在Node環境下你能夠安裝你須要的包 來構建你的程序。

相關文章
相關標籤/搜索