人人都看得懂的JS運行機制

前言

本文是以做者本身理解的思路拆分的JS運行機制,看法有限不免疏漏,歡迎留言勘誤、交流。javascript

瀏覽器

瀏覽器的主要功能是向服務器發送請求,在窗口中展現目標網絡資源。
伴隨着瀏覽器的的普及,Javascript是做爲瀏覽器的附屬工具誕生的,當初主要是爲了作瀏覽器端的簡單校驗。前端

瀏覽器的主要功能能夠總結爲java

  • 展現資源
  • 功能交互

瀏覽器內核

瀏覽器的內核是支持瀏覽器運行的最核心的程序。 對應着上述瀏覽器的主要功能, 主要有兩個部分:node

  • 渲染引擎
  • Javascript引擎

渲染引擎

渲染引擎: 將HTML、CSS、Javascript文本及相應的資源轉換成圖像結果。
其主要做用解析資源文件並渲染在屏幕上。
同時,它也是咱們一般所說的狹義的瀏覽器內核web

想一想看,說起瀏覽器內核咱們立馬想到的是什麼?chrome

  • Webkit (safari、老版本的chrome)
  • Trident (老朋友IE)
  • Gecko (熟悉的陌生人Firefox)
  • Blink (新版本的chrome)

它們的主要工做內容就是根據咱們HTML、CSS、JS的定義,繪製出相應的頁面結構及展示形式。api

Javascript引擎

1.是什麼?爲何?作什麼?

Javascript 是解釋型語言,在代碼運行以前不會進行編譯工做,將源碼轉換成字節碼等中間代碼或者是機器碼,
而是在執行的過程當中實時編譯,邊編譯邊執行。瀏覽器

所以須要一個功能模塊作編譯轉換相關的工做,Javascript引擎就是來作這些事兒的。
總結來講就是: 運行過程當中解析JS源代碼並將其轉換成可執行的機器碼並執行服務器

解釋型語言 && 編譯型語言
解釋型: 代碼運行以前,不須要編譯,而是在執行過程當中現編譯,邊編譯邊執行
編譯型: 代碼在運行以前,須要先用編譯器將其轉換成機器語言或者中間字節碼,而後再執行
一般狀況下,說解釋型語言慢,就是由於編譯過程發生在執行過程當中網絡

2.常見的Javascript引擎

常見的Javascript引擎以下:

  • Chrome V8引擎 (chrome、Node、Opera)
  • SpiderMonkey (Firefox)
  • Nitro (Safari)
  • Chakra (Edge)

BUT, 僅僅依靠Javascript引擎是不能勝任瀏覽器的JS處理工做的,事實上Javascript引擎只是瀏覽器處理JS相關的模塊中的一個小積木.

運行時Runtime

在Web開發中,咱們一般不會直接用到Javascript引擎。 事實上,Javascript引擎是工做在一個環境(容器)內的, 這個環境提供了一些額外的功能(API),咱們的代碼在執行的時候可使用這些特性。

Stack overflow 上有一個高讚的回答,關於Javascript引擎和Javascript Runtime。

在這個觀點中,不像其餘編譯型語言,編譯以後成爲機器碼能夠直接在主機上運行。 JS是運行在一個宿主環境中的(一個能夠識別而且執行JS代碼的程序),這個容器通常必需要作2件事情:

  • 解析JS代碼,轉換成可執行的機器語言
  • 暴露一些額外的對象(API),能夠與JS代碼作交互

作第一部分工做的就是Javascript引擎
作第二部分功能的實際上就是咱們本節所說的 Javascript Runtime, 所以咱們能夠粗略地理解爲, Javascript Runtime 就是JS宿主環境建立的一個scope, 在這個scope內JS能夠訪問宿主環境提供的一系列特性

常見的JS宿主環境有什麼?

  • web 瀏覽器
  • node.js

node

那麼在這兩個環境中,對應的引擎和runtime分別以下:

宿主環境 JS引擎 運行時特性
瀏覽器 chrome V8引擎 DOM、 window對象、用戶事件、Timers等
node.js chrome V8引擎 require對象、 Buffer、Processes、fs 等

相同的JS引擎,只是在不一樣的環境下,提供了不一樣的能力。

總之, 最初的js被設計出來,只是爲了作網頁校驗的,可是經過不一樣環境下的Javascript RuntimeJS能夠作更多的工做。

至此,關於JS運行時,咱們有一個簡單的瞭解。

接下來,咱們再以瀏覽器環境中代碼執行的邏輯爲思路,看一看JS的運行機制。

JS引擎

根據上文的描述,JS源代碼是須要通過JS引擎解析、轉化以後才執行的。
一般認爲,JS引擎主要有兩部分組成:

  • 內存堆 引用類型實際值、內存分配的地方
  • 調用棧 基本類型存儲、引用類型地址名存儲、代碼邏輯執行的地方

源代碼進入JS引擎以後,順序讀取代碼,按照變量聲明、函數執行等不一樣規則,分配到堆、或者棧內。

JS引擎

內存堆

存儲項目引用類型數據的地方, 系統分配的內存,
JS中的引用類型數據,實際值是零散地存在這裏面的
事實上,引用類型的存儲是分爲2部分存儲的:

  • 真實值存儲在內存中, 是系統根據自身狀況,內存區哪裏有合適的位置,就分配在哪裏,沒有嚴格的順序的,所以說是零散的
  • 真實值所在的物理內存地址,這個值是以基本值的形式存儲在棧內的

平時代碼中的引用類型賦值,就是僅僅把棧內存儲的內存地址賦給新變量,就至關因而告訴新變量該值在內存中的位置,須要的時候去取就行,並非把真正的值傳遞過去,內存中該值是隻有一份的。這也是引發引用問題的緣由

執行棧

執行棧,是代碼中實際邏輯語句執行的地方,同時項目運行過程當中產生的基本類型的值也是存在此處。

引擎會把代碼分紅一個個可執行單元,而後依次進入執行棧,被執行

那麼可執行單元是什麼?

可執行單元,標準的說法是執行上下文
JS中,執行上下文能夠是如下幾種:

  • Global code -----> 全局執行上下文
  • Function code -----> 函數執行上下文
  • Eval code -----> eval函數執行上下文

這些東西有什麼共同點?

全局代碼能夠看做是一個IIFE(當即執行函數),
函數就是通俗意義上的函數
eval 是能夠把傳入字符串執行的函數

函數啊, 所有都是函數啊

所以咱們能夠粗略的理解爲: 執行棧裏面的東西,都是一個個函數調用

  • 首先是入口文件的所有JS代碼做爲一個IIFE,最早入棧被調用
  • 而後在實際執行過程當中,調用了其餘函數,就會順次被壓入棧內執行

所以, JS引擎的示意圖能夠更新爲以下:

單線程

JS是單線程的。 地球人都知道。

什麼意思?
JS引擎中,代碼執行是在調用棧的裏發生的。
棧是一種LIFO(last in first out 後進先出)的數據結構。
只有棧頂的函數會被處理, 處理完成以後彈出棧, 後面的進入棧頂,再被執行...

舉個🌰:

以下的JS文件

function first() {
  second();  
}

function second() {  
  console.log('log fn');  
}

first();

... // 後續操做
複製代碼

入棧示意圖

JS引擎處理這段代碼的步驟以下 (只關注函數調用)

  1. 整段代碼做爲一個IIFE,入棧, main()函數調用進入棧頂,開始執行
  2. 遇到first函數調用, first函數進入棧頂,開始進入first函數體執行 (此時main函數還未執行完畢)
  3. 進入first函數體以後,遇到調用second函數, 把second函數壓入棧頂,進入second函數體執行
  4. 進入second函數體以後,遇到console.log函數調用, 把console.log壓入棧頂執行
  5. console.log函數打印完畢以後,執行結束, 彈出棧頂
  6. 此時棧頂是second函數,繼續執行second函數體中,console.log以後的代碼,發現沒有可執行代碼了,OK,那就宣告second執行完畢,彈出
  7. 此時first函數進入棧頂,那就執行first函數體中,second函數調用以後的代碼,發現空空如也,那麼first函數執行完畢,彈出
  8. 棧頂main函數執行後續代碼

所以JS單線程,指的是在JS引擎中,解析執行JS代碼的調用棧是惟一的,全部的JS代碼都在這一個調用棧裏按照調用順序執行,不能同時執行多個函數

單線程意味着什麼?

意味着,正常狀況下,JS引擎會按照代碼書寫的邏輯,依次調用函數,在任意時間點,有且只能有一個函數被執行。外層函數必須等到內層函數處理完畢有返回值以後才能繼續執行。

爲何要單線程?
JS最初被設計使用在瀏覽器上,做爲瀏覽器上的腳本語言,須要與用戶的操做互動以及操做DOM, 若是時多線程的話,須要關注各個線程之間狀態的同步問題。

想象一下,js引擎中能夠同時執行2個函數,若是兩個函數操做同一個對象,那麼到底以哪一個爲準?
而後,操做DOM結構,在線程A上已經刪了某節點,線程B同時還在對該節點一頓操做,這就尷尬了。

而作多線程的狀態同步又是得不償失的,所以就直接用單線程了。

有什麼問題?

假如,某個函數耗時比較久,那麼調用它的外層函數必須安安靜靜的等待這個函數執行完成才能繼續執行, 若是,這個函數出錯了,那麼外層的函數也沒辦法繼續執行了。

function foo(){
  bar()
  console.log('after bar')
}
function bar(){
  while(true) {}
}

foo()
複製代碼

好比這個栗子,foo函數中調用了bar函數,那麼foo函數必須等待bar函數執行完畢才能繼續執行後續代碼,然而bar函數是個無限執行的函數喲,回不來的,那麼foo函數等到花兒都謝了也沒辦法執行後面的代碼的。

固然這是比較極端的狀況,可是在前端的業務場景中有幾類常見的case,在此是有問題的:

  • 定時器延遲操做
  • 網絡請求
  • 網頁事件等

這些就是咱們常見的異步操做, 事件觸發以後並不能當即獲得結果,按照以前的運行模式,瀏覽器就會阻塞其餘操做,等待相應結果,表如今頁面中就是頁面卡死,這是一個優秀的應用所不能容許發生的。

同步&異步
關於同步和異步操做,借用樸靈大神的說法:
通常操做能夠分爲兩個步驟,

  1. 發起調用
  2. 獲得結果

發起調用,立馬能夠獲得結果的是爲 同步
發起調用,沒法當即獲得結果,須要額外操做才能獲得結果的是爲 異步

所以當前的模型在異步操做中是有問題的

解決方案?
問題的本質是js引擎的單線程工做模式,只專一於一件事情, 必須【執行至完成】
而產生問題的那些操做每每不能直接獲得 結果,必須通過額外操做才能獲得結果 【異步問題】

思路:能夠把這些異步操做分發給其餘模塊,獲得處理結果以後再把回調函數一塊放入主線程執行。
這就是 事件循環(Event Loop)的主體思路。

event loop 只是解決異步問題的一種思路 其餘的思路還有:

  • 輪詢
  • 事件

Web API's

上一章節提到,異步操做能夠交給JS引擎以外的其餘模塊處理, 在瀏覽器中其餘模塊就是Web API模塊

Web API 其實就是上述的 JS runtime 提供的一系列宿主環境的特性集合。

在瀏覽器中,主要包括如下能力:

  • Event Listeners
  • HTTP request
  • Timing functions

完美的cover了上述產生異步問題的幾類case

所以至此,咱們能夠得出如下的視圖:

JS引擎中,執行棧遇到同步函數調用,直接執行獲得結果後,彈出棧,繼續下一個函數調用

遇到異步函數調用,將函數分發給Web API模塊,而後該異步函數彈出棧,繼續下一個函數調用,不會產生阻塞問題。

問題來了?

這些異步操做分發給Web API模塊處理以後,不能說無論了,主線程仍是須要知道結果作後續操做的,Web API獲得結果以後怎麼通知主線程呢?

這就須要其餘的模塊幫忙了。

這個地方提到了主線程,就意味着還有其餘的輔助線程。
是的,JS是單線程執行的,可是並不意味着瀏覽器內核是單線程的。
事實上,web api模塊內就有多個線程,每一個異步操做處理模塊都對應一個線程
http請求線程、事件處理線程、定時器處理線程等

回調隊列 (callback queue)

回調隊列, 也叫事件隊列、消息隊列。
這個模塊就是用來幫助Web API模塊處理異步操做的。

Web API模塊中,異步操做在相應的線程中處理完成獲得結果以後,會把結果注入異步回調函數的參數中,而且把回調函數推入回調隊列中。

可是,只推到回調隊列裏也不是個事兒,由於前面說到了,全部的JS執行都發生在主線程調用棧裏面。這些異步操做拿到結果以後,帶着回調函數推入了回調隊列,須要在適當的時機進入主線程調用棧執行。

那麼,誰知道何時是合適的時機呢?

Event Loop 知道。

回調隊列
隊列是一個FIFO,先進先出的存儲結構,
這樣意味着異步操做的回調函數會按照進入隊列的順序被執行,而不是調用的順序被執行

Event Loop

Event Loop 不停地檢查主線程調用棧和回調隊列,當發現主線程空閒的時候,就把回調隊列裏第一個任務推入主線程執行。 以此不停地循環。

至此,一個異步操做,兜兜轉轉最終拿到告終果,成功執行而且沒有阻塞其餘的操做。

overview

一個完整的圖示以下:

至此,本文在宏觀結構上按照瀏覽器執行js文件的步驟,分析了瀏覽器環境的一些簡單機制。 更多的執行細節,好比詞法分析、做用域構建等在後續文章中會繼續深刻。

關於咱們

快狗打車前端團隊專一前端技術分享,按期推送高質量文章,歡迎關注點贊。
文章同步發佈在公衆號喲,想要第一時間獲得最新的資訊,just scan it !

公衆號二維碼

參考文章

相關文章
相關標籤/搜索