瀏覽器如何運行 JavaScript

瀏覽器如何運行 JavaScript

須要鋪墊的知識點:執行環境 + 執行棧web

執行環境(Execution Context)

執行環境是函數被調用時所在的環境。執行環境中存儲了函數執行時相關的全部事物。當咱們在函數內訪問某一變量時,這個變量其實也是該函數執行環境提供的。由於執行環境是不能直接被訪問的(不能被訪問表明咱們在函數內部不能使用任何變量),因此ECMA標準在函數被調用時,會構造一個可以被訪問的對象——執行環境對象(Execution Context Object),這個對象上包含三個屬性:變量對象、做用域鏈、this的值。瀏覽器

這三個屬性的做用:數據結構

  1. 變量對象:提供對「與執行環境相關的變量和函數」的訪問和存儲的一個對象,也就是當前做用域。(Variable Object 下文簡稱VO)

在全局執行環境中,VO 就是全局對象。在函數執行環境中,由於 VO 不能被直接訪問的,這是會提供一個活動對象(Activation Object)簡稱 AO 來扮演 VO 的角色。AO 在進入函數執行環境的那一刻被建立。 函數接收到的參數,存儲在變量對象的arguments屬性上。 函數內部定義的變量和函數,都會在變量對象上擁有一個同名屬性,變量的屬性值爲變量值,函數的屬性值爲該函數的指針。多線程

  1. 做用域鏈:當咱們在函數內部訪問某一變量時,就會在做用域鏈上進行查找。查找順序是從做用域鏈頂部到最外層做用域也就是全局做用域,當找到變量或者找到全局做用域爲止。

做用域是用來查找標識符的一種規則。決定了在當前函數內聲明的標識符的可做用範圍。 若是發生做用域嵌套,則會造成做用域鏈。做用域鏈包括當前做用域(也就是當前執行環境的變量對象)加上外層做用域鏈。 每一個函數對象的做用域鏈都被存儲在其內部屬性[[Scope]]上,做用域鏈上的外部做用域是經過複製外部函數的[[Scope]]屬性構成的。 即Scope Chain = [AO].concat([[Scope]])dom

  1. this的值:由於函數能夠在不一樣的執行環境被執行,因此 JS 設計出了 this 的概念,用於指向真正運行的執行環境。

當函數做爲某個對象的屬性調用時,this指向那個屬性。當函數自主調用時,this指向undefined,在非嚴格模式下,this又指向全局對象,在瀏覽器中則是window對象。異步

執行環境分爲三種:async

  1. 全局執行環境(Global Execution Context)
    全局執行環境是JS代碼一被加載時,就會生成的默認執行環境。是最外圍最大的執行環境,有且僅有一個。瀏覽器在加載 JS 代碼時,指定 window爲全局執行環境的變量對象,全部變量和函數都是定義在window 對象上的某個屬性。 全局執行環境是一直存在的,直到瀏覽器關閉窗口後,纔會被銷燬。
  2. 函數執行環境(Functional Execution Context)
    函數執行環境又叫作局部執行環境,顧名思義,就是 JS 引擎在識別到有函數被調用,即會建立出的一個函數執行環境,能夠有多個。當一個函數被執行完畢時,它所在的執行環境就會被銷燬。
  3. Eval 執行環境
    在 eval 函數中的執行環境。

執行棧(Execution Stack)

瀏覽器用 JS 引擎執行 JS 代碼,而 JS 引擎構建出執行棧用來記錄程序運行狀況。執行棧遵循棧數據結構,先進先出,每當遇到一個函數調用,就會建立出其執行環境壓入執行棧棧頂,當函數執行完成後將其推出執行棧。保證執行流按照執行棧的順序有序執行。編輯器

須要鋪墊的知識點:瀏覽器的多個線程函數

JS 是單線程

JS 是單線程語言意味着只會有一條線程在執行 JS,一次也只會執行一個 JS 任務,其他任務都須要排隊等待上一個任務執行完畢才能執行。oop

同步 & 異步

同步:每一個任務按照順序進行執行,必須等待前一個任務執行完畢,後一個任務才能夠開始。

異步:能夠將一個任務分紅兩段,先執行第一段,而後執行其餘任務,等作好了準備,再來執行第二段。

爲何 JS 要設定爲單線程

單線程的特色就是同一時間只能作一件事情,而 JS 是被做爲瀏覽器腳本語言使用的,主任務是提供用戶與頁面交互的能力。設想一下若是瀏覽器是多線程,有兩個線程同時在對一個 dom 進行操做,這時瀏覽器就會不知道以哪一個線程爲準。

爲何須要異步任務

咱們知道,瀏覽器上有不少操做是很耗時的,好比請求數據、加載媒體文件等,若是都是同步任務,則須要等待一個一個耗時操做的完成,而相對次要的耗時操做其實不該該讓用戶有等待的感受,用戶體驗會很是的很差。因此會經過一些其餘線程來實現異步的形式。

瀏覽器運行 JS 時涉及到的幾個線程

  1. JS 引擎線程:用於執行 JS 代碼,JS 引擎有多個線程,由一個主線程和 n 個其餘線程配合一塊兒工做。主線程用於執行當前執行棧內的任務。
  2. 事件觸發線程:用來存放異步事件,每個異步事件觸發後,都會交給事件觸發線程管理,造成一個任務隊列。
  3. 定時任務線程:用於管理設定的定時任務,到達設定事件後,會將定時任務的回調函數推到事件觸發線程管理的任務隊列上。

瀏覽器運行 JS 的工做流程

至關於執行棧與以上三個線程的工做流程

  1. 瀏覽器加載 JS 代碼後,JS 引擎主線程會構建出一個執行棧,用於主線程讀取當前可執行任務。
  2. 執行流按行讀取JS代碼,每遇到一個函數調用便會建立出一個新的執行環境並將其壓入執行棧棧頂。
  3. 主線程讀取執行棧棧頂的任務進行執行,執行流進入函數內部開始執行,執行完成後將執行環境作出棧操做,執行流回到下一個執行環境開始執行。
  4. 主線程只會執行同步任務代碼,當遇到異步事件時會將其託管給對應的其餘線程代爲執行,當異步事件知足執行回調的條件時,會將其回調函數放入事件觸發線程管理的任務隊列的末尾,當可執行棧內爲空時,主線程纔會去讀取任務隊列列頭的任務壓入執行棧進行執行。JS 的工做流程就是反覆上述執行過程。

主線程在執行棧和任務隊列進行反覆輪詢的過程就是咱們常說的 JS 執行機制 —— 事件循環(Event Loop)。

微任務 & 宏任務

JS 又把異步任務分爲了2類:宏任務 + 微任務

全部異步任務一開始都會被彙總到一個事件列表(Event Table)中,當知足塞入事件隊列(Event Queue)的條件時(好比異步請求完成、定時任務到達時間等),會將事件從 Event Table 中取出,並按照該異步任務類型將其回調函數放入到宏任務事件隊列微任務事件隊列中,JS 引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空讀取事件隊列時,會優先讀取微任務事件隊列上的全部事件,並壓入執行棧開始執行,然後執行棧又爲空時再讀取宏任務事件隊列列頭的第一個任務壓入執行棧,開啓第二輪事件循環

在執行機制上兩者的區別:

  1. 微任務處理優先級高於宏任務。
  2. 讀取微任務事件隊列時會一會兒讀取一整個隊列,而讀取宏任務事件隊列時只會取出第一個任務壓入執行棧執行。

常見的一些異步任務分類

  1. 宏任務(Marco Task): script中的代碼、 setTimeoutsetInterval
  2. 微任務(Micro Task): Promise

運行機制流程圖

任務隊列裏有啥?

Javascript 任務分爲同步任務和異步任務,同步任務是前一個任務結束後一個任務才能開始。異步任務則不用等前一個任務完成就能夠開始。

而異步任務又包括異步請求、異步回調(dom操做的回調、定時任務的回調)等,這些任務都會被放置在任務隊列中。

對於異步請求和定時任務會先交給瀏覽器代爲處理,等處處理完畢後將異步任務的回調函數推入任務隊列的末尾,等待主線程讀取。

async/await

講到主線程和執行棧,就有一個不得不提的內容 async/await

async函數會返回一個Promise對象,便於回調函數管理,await是一個運算符,用於組成表達式,await xxx的計算結果取決於await它等待的東西,也就是xxx。若是它等待的不是一個Promise,那麼它的計算結果就是它等待的東西。

await等待的是一個Promise時,它會對當前await後面聲明的代碼進行阻塞,直到Promise返回後纔會繼續執行後續代碼。

當執行流遇到await functionXX(): Promise<any>時,由於發生了函數調用,因此functionXX會被壓入執行棧,執行流會進入到函數內部,將return Promise以外的代碼先執行一遍,Promise相關的異步操做會交由瀏覽器執行。

舉個定時任務的例子

當咱們設定了一個 10s 的定時任務,瀏覽器定時觸發線程中的計數器在 10s 後準時的將定時任務的回調函數添加到任務隊列末尾。

到這一步都是很正常的,可是這個被添加進消息隊列的回調函數何時會被讀取呢?

只有當主線程執行完全部任務後纔會依次讀取任務隊列中的任務。

也就是說雖然瀏覽器按時的將定時任務發送到了任務隊列中,但真正被主線程讀取的時間可能超過了設定的時間。這也就是爲何有些定時任務被執行的時間和設定時間不一致的緣由。



寫在最後

本篇內容以理論爲主,後續還會開一篇內容專門用代碼作例子分析。本文的所有內容,純本人理解後手敲的文字,有不對的地方,歡迎指出和糾正,感謝閱讀,歡迎點贊 🦀🦀

本文使用 mdnice 排版

相關文章
相關標籤/搜索