js——事件循環

  • 2020 年 11 月 5 日
  • 筆記

JS—事件循環

js運行的環境稱之為宿主環境。

執行棧 :call stack ,一個數據結構,用於存放各種函數的執行環境,每一個函數執行之前他的相關信息會加入到執行棧中,函數調用之前,創建執行環境,然後加入到執行棧中;函數調用之後,銷毀執行環境

function a (){
    console.log("a")
    b()
}
function b(){
    console.log("b")
    c()
}
function c(){
    console.log("c")
}
console.log("global")
a()

執行順序: global  a  b  c  

答案顯而易見,但是為什麼會這樣呢?

整個js運行了之後 就只有一個執行棧,首先整個js定義了三個函數,js代碼在運行的時候都會先初始化一個全局上下文GO,然後把GO放入在call stack 中,接着console.log()函數執行,創建一個log的上下文放入call stack,輸入「global」之後log的上下文就會銷毀,然後a()函數執行,創建一個a的上下文放入call stack,輸出”a”,b()執行,這時a函數還沒有結束,因為a函數裏面還需要執行b函數,所以a的上下文還留在call stack中,b() 函數執行,創建一個b的執行上下文放入call stack 中,b函數出入”b”,c(),這時b函數也還沒有結束,還需要執行c()函數,所以b的上下文也還在call stack中沒有銷毀,c()函數執行,創建一個c的上下文放入到call stack中,c函數輸出”c”後,c函數結束,call stack裏面的c的上下文銷毀,c函數執行完,b函數也就執行完了所以,接着,b的上下文也接着銷毀,b函數執行完後a函數也就代表着執行完,所以a的上下文也接着被銷毀,最後這整個js代碼運行完後call stack裏面最先初始化創建的全局上下文就銷毀。

再來一個例子鞏固一下上面的原理:求斐波拉契數列 第一個和第二個數固定為1,之後任意一個數都是前兩位數之和

function getFeibo(n){
    if(n ===1 || n===2 ){
        return 1;
    }
    return getFeibo(n-1) + getFeibo(n-2)
}
console.log(getFeibo(3))

首先初始化一個全局上下文放入call stack 中 log()執行 創建一個log的執行上下文放入call stack 中 這時getFeibo(3)執行,創建一個n=3的getFeibo的執行上下文,n=3沒有進入if語句,執行getFeibo(3-1)函數,創建一個n=2 的getFeibo的執行上下文,進入if語句 返回1,n=2的getFeibo的執行上下文銷毀,return的前部分結束,執行 getFeibo(3-2)函數,創建一個 n=1 的getFeibo的執行上下文, 進入if語句,返回1,n=1的getFeibo的執行上下文銷毀,return的後部分結束 n=3的getFeibo函數運行結束返回2 ,n=3的getFeibo的執行上下文銷毀,最後初始化的那個全局上下文也銷毀這時call stack清空。

 

js引擎永遠執行的都是執行棧的最頂部

 

異步函數:某些函數不會立即執行,需要等到某個時機到達後才會執行,這樣的函數就被稱之為異步函數,比如時間處理函數。異步函數的執行時機,會被宿主環境控制。

瀏覽器宿主環境中包含有5個線程:

  1. JS引擎:負責執行執行棧的最頂部代碼
  2. GUI線程:負責渲染頁面
  3. 事件監聽線程:負責監聽各種事件
  4. 計時線程:負責計時(setTimeout、setInterval)
  5. 網絡線程:負責網絡通信 如 ajax

 

當上面的線程發生了某些事情,如果該線程發現,這件事情有處理程序,他就會將該處理程序放入到一個叫做事件隊列的的內存里,當JS引擎發現,執行棧call stack 中裏面已經沒有任何內容後,call stack就會把事件隊列中的第一個函數加入到執行棧中去執行

例如:

function a(){
    console.log("a")
    setTimeout(()=>{
        b()
    },0)
    c()
}
function b(){
    console.log("b")
}
function c(){
    console.log("c")
}
console.log("global")
a()

輸出順序:global a c b

首先初始化一個全局執行上下午放入call stack中 log()函數執行,創建一個log的執行上下文,輸出global,log的執行上下文銷毀,a函數執行,創建一個a的執行上下文,log執行創建一個log的執行上下文,輸出a,log的執行上下文銷毀,setTimeout 開啟一個定時器告訴宿主環境0秒後執行b函數,注意這時b函數還沒有執行,就緊接着執行c函數

創建一個c的執行上下文,log執行創建一個log的執行上下文,輸出c,log的執行上下文銷毀,這時a函數裏面的代碼執行完 a的執行上下文被銷毀,整個js代碼執行完畢,全局上下文也從call stack被銷毀,call stack清空。這時有人會問 誒 b函數不是還沒有執行嗎怎麼說整個js代碼執行完畢了呢? 是這樣的 setTimeout開啟了一個定時器,這時已經是計時線程發現了有處理程序,會告訴宿主環境 0 秒後執行b函數,宿主環境知道後就會在0秒過後把 b函數放入到一個叫事件隊列的內存中。所以當call stack 裏面的內容清空後(即當最開始初始化的全局上下文被銷毀後)JS引擎會從事件隊列中取出處理程序來執行。

 

 

JS引擎從事件隊列中取出處理程序來執行,以及與宿主環境的配合,稱之為事件循環

 

事件隊列在不同的宿主環境中有所差異,大部分宿主環境會將事件隊列進行細分。在瀏覽器中,事件隊列被分為兩種:

  1. 宏隊列:macroTask,計時器結束的回調、事件回調、http回調等等絕大部分異步函數是進入宏隊列
  2. 微隊列:MutationObserver,Promise產生的回調進入微隊列

當執行棧清空是,JS引擎首先會將微任務中的所有任務一次執行結束,如果沒有微任務,則執行宏任務。