須要鋪墊的知識點:執行環境 + 執行棧web
執行環境
是函數被調用時所在的環境。執行環境中存儲了函數執行時相關的全部事物。當咱們在函數內訪問某一變量時,這個變量其實也是該函數執行環境提供的。由於執行環境是不能直接被訪問的(不能被訪問表明咱們在函數內部不能使用任何變量),因此ECMA
標準在函數被調用時,會構造一個可以被訪問的對象——執行環境對象(Execution Context Object),這個對象上包含三個屬性:變量對象、做用域鏈、this的值。瀏覽器
這三個屬性的做用:數據結構
在全局執行環境中,VO 就是全局對象。在函數執行環境中,由於 VO 不能被直接訪問的,這是會提供一個活動對象(Activation Object)簡稱 AO 來扮演 VO 的角色。AO 在進入函數執行環境的那一刻被建立。 函數接收到的參數,存儲在變量對象的
arguments
屬性上。 函數內部定義的變量和函數,都會在變量對象上擁有一個同名屬性,變量的屬性值爲變量值,函數的屬性值爲該函數的指針。多線程
做用域是用來查找標識符的一種規則。決定了在當前函數內聲明的標識符的可做用範圍。 若是發生做用域嵌套,則會造成做用域鏈。做用域鏈包括當前做用域(也就是當前執行環境的變量對象)加上外層做用域鏈。 每一個函數對象的做用域鏈都被存儲在其內部屬性
[[Scope]]
上,做用域鏈上的外部做用域是經過複製外部函數的[[Scope]]
屬性構成的。 即Scope Chain = [AO].concat([[Scope]])
dom
當函數做爲某個對象的屬性調用時,
this
指向那個屬性。當函數自主調用時,this
指向undefined
,在非嚴格模式下,this
又指向全局對象,在瀏覽器中則是window
對象。異步
執行環境分爲三種:async
window
爲全局執行環境的變量對象,全部變量和函數都是定義在window 對象上的某個屬性。 全局執行環境是一直存在的,直到瀏覽器關閉窗口後,纔會被銷燬。
瀏覽器用 JS 引擎執行 JS 代碼,而 JS 引擎構建出執行棧,用來記錄程序運行狀況。執行棧遵循棧數據結構,先進先出,每當遇到一個函數調用,就會建立出其執行環境壓入執行棧棧頂,當函數執行完成後將其推出執行棧。保證執行流按照執行棧的順序有序執行。編輯器
須要鋪墊的知識點:瀏覽器的多個線程函數
JS 是單線程語言意味着只會有一條線程在執行 JS,一次也只會執行一個 JS 任務,其他任務都須要排隊等待上一個任務執行完畢才能執行。oop
同步:每一個任務按照順序進行執行,必須等待前一個任務執行完畢,後一個任務才能夠開始。
異步:能夠將一個任務分紅兩段,先執行第一段,而後執行其餘任務,等作好了準備,再來執行第二段。
單線程的特色就是同一時間只能作一件事情,而 JS 是被做爲瀏覽器腳本語言使用的,主任務是提供用戶與頁面交互的能力。設想一下若是瀏覽器是多線程,有兩個線程同時在對一個 dom 進行操做,這時瀏覽器就會不知道以哪一個線程爲準。
咱們知道,瀏覽器上有不少操做是很耗時的,好比請求數據、加載媒體文件等,若是都是同步任務,則須要等待一個一個耗時操做的完成,而相對次要的耗時操做其實不該該讓用戶有等待的感受,用戶體驗會很是的很差。因此會經過一些其餘線程來實現異步的形式。
至關於執行棧與以上三個線程的工做流程
主線程在執行棧和任務隊列進行反覆輪詢的過程就是咱們常說的 JS 執行機制 —— 事件循環(Event Loop)。
JS 又把異步任務分爲了2類:宏任務 + 微任務
全部異步任務一開始都會被彙總到一個事件列表(Event Table)中,當知足塞入事件隊列(Event Queue)的條件時(好比異步請求完成、定時任務到達時間等),會將事件從 Event Table 中取出,並按照該異步任務類型將其回調函數放入到宏任務事件隊列或微任務事件隊列中,JS 引擎存在monitoring process
進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空讀取事件隊列時,會優先讀取微任務事件隊列上的全部事件,並壓入執行棧開始執行,然後執行棧又爲空時再讀取宏任務事件隊列列頭的第一個任務壓入執行棧,開啓第二輪事件循環。
在執行機制上兩者的區別:
微任務處理優先級高於宏任務。 讀取微任務事件隊列時會一會兒讀取一整個隊列,而讀取宏任務事件隊列時只會取出第一個任務壓入執行棧執行。
常見的一些異步任務分類
script
中的代碼、
setTimeout
、
setInterval
Promise
Javascript 任務分爲同步任務和異步任務,同步任務是前一個任務結束後一個任務才能開始。異步任務則不用等前一個任務完成就能夠開始。
而異步任務又包括異步請求、異步回調(dom操做的回調、定時任務的回調)等,這些任務都會被放置在任務隊列中。
對於異步請求和定時任務會先交給瀏覽器代爲處理,等處處理完畢後將異步任務的回調函數推入任務隊列的末尾,等待主線程讀取。
講到主線程和執行棧,就有一個不得不提的內容 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 排版