JavaScript中的事件循環機制:你不得不懂的JS原理

事件循環

學過JS的都知道,JS是單線程的,即使html5中提出了woker,但它依舊在主線程的控制之下,只能進行計算任務,而不能操作dom等,所以worker並沒有改變JS是一個單線程這一機制。單線程即是後一個任務必須要等待前一個任務執行完畢才能執行,如果執行像setTimeout延遲器,亦或者異步任務等,都不會消耗cpu,就會有空等的情況,爲了更好的協調事件、腳本、UI渲染等行爲,於是有了事件循環機制。

在瞭解事件循環之前,先了解幾個概念:

JS:一門計算機語言,提供了表達程序邏輯的語法和實現基本功能的API

瀏覽器:JS語言的真實運行環境,又稱之爲JS的宿主環境

JS執行引擎:JS宿主環境(例如瀏覽器)中的一個功能模塊,用於解析並執行JS

進程:當一個應用程序運行時,需要使用內存和CPU資源,這些資源需要向操作系統申請。操作系統以進程的方式來分配這些資源,一個進程就代表着一塊獨立於其他進程的內存空間。一個應用程序要運行,必須至少有一個進程啓動。進程的最大特點是獨立,一個進程不能隨意的訪問其他進程的資源。這就保證了多個程序在操作系統上運行互不干擾。

在這裏插入圖片描述

線程:可能要同時執行多個任務,每個任務需要在一個線程上運行,線程與線程之間相對獨立,但可以共享應用程序的進程數據。

在這裏插入圖片描述

瀏覽器中的線程

  • JS執行線程:負責執行執行棧的最頂部代碼

  • GUI渲染線程:負責渲染頁面

  • 事件監聽線程:負責監聽各種事件

  • 計時線程:負責計時

  • 網絡http線程:負責網絡通信

單線程與多線程

我們之所以稱JS爲單線程的語言,是因爲它的執行引擎只有一個線程,並且不會在執行期間開啓新的線程。而並非瀏覽器是單線程的

單線程的應用程序的優點

  • 易於學習和理解:所有代碼都是按照順序從上到下執行的
  • 易於掌控程序:由於代碼都按照順序執行,不會出現中斷,也沒有共享資源的爭奪問題,極大的降低了開發難度。
  • 更加合理的利用計算機資源:創建新的線程和銷燬線程都會耗費額外的CPU和內存資源,沒有良好的線程設計,將導致程序運行效率低下。而單線程的應用不受此影響

任何一個程序在執行期間都可能會開啓多個任務,比如:

var count = 0
var btn = document.getElementsByClassName("btn")[0];
console.log(count);

setInterval(() => {
    count++
    console.log(count)
}, 1000);


btn.onclick = function(){
    count++
    console.log(count)
}

這段代碼開啓了三個任務:

任務1:程序啓動時開始進行一些操作

任務2:開啓一個計時器,每隔一段時間去做一些事

任務3:監聽按鈕是否被點擊,當按鈕被點擊後,去做一些事

我們分別來看一下,單線程和多線程的對於相同的任務,有什麼區別吧。

多線程

在這裏插入圖片描述

可以看到,多線程是一個任務開啓一個線程,如果以多線程的方式運行,會導致程序代碼在某些時候會有重疊執行的情況出現,如果這些代碼湊巧在使用共享數據,將難以控制最終的運行結果。

單線程

在這裏插入圖片描述

可以看出,JS單線程執行,也是配合了瀏覽器中的其他線程,但是所有的JS代碼都在單個線程中執行,不會出現多個任務同時執行的情況,自然就不會出現資源爭奪的問題。

同步代碼:程序啓動後,在JS執行線程上立即執行的任務代碼。下面的代碼都是同步代碼

function print(callback){
    console.log(1)
    callback()
}
print(function(){
    console.log(2)
})
console.log(3)

異步代碼:收到宿主環境(瀏覽器)的其它線程通知,即將在JS執行線程上執行的代碼,例如計時器回調函數中的代碼,事件中的代碼,Promise回調的代碼,網絡請求的代碼都是異步的。JS中的異步代碼往往放到一個函數中,該函數成爲異步函數(該函數是異步的)。

執行棧:爲了保證JS代碼有序的執行,JS執行引擎使用執行棧來組織JS代碼。每當調用一個函數時,都會在執行棧中創建一個執行上下文,上下文中提供了函數執行需要的環境,創建了上下文之後,再執行函數。是不是感覺說的不是人話了,哈哈哈,看個例子加配圖就明白了。我把以下代碼在執行棧運行圖畫了出來。

console.log("1")

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

a()

只要是JS執行引擎執行代碼,在執行棧中就會創建全局上下文,等JS執行完所有代碼後,銷燬全局上下文,如上面代碼,執行完最後一行a()函數後,銷燬全局上下文,代碼執行結束。JS引擎永遠執行的是執行棧的最頂部

在這裏插入圖片描述
在這裏插入圖片描述
事件循環:是JS處理異步函數的具體方法。文章的重點來啦

事件循環是JS處理異步函數的具體方法:

  1. JS執行引擎執行 執行棧 中的代碼
  2. 遇到一些特殊代碼交給瀏覽器的其他線程處理
  3. 將執行棧中的代碼全部執行完畢
  4. 從事件隊列中取出第一個微任務,無微任務取出第一個宏任務,放入執行棧,然後重複第1步

在這裏插入圖片描述
事件隊列在不同的宿主環境中有所差異,大部分宿主環境會將事件隊列進行細分。在瀏覽器中,事件隊列分爲兩種:

  • 宏任務(隊列)macroTask:計時器結束的回調、事件回調、http回調等等絕大部分異步函數進入宏隊列
  • 微任務(隊列)microTask:Promise的回調, MutationObserver(用於監聽某個DOM對象的變化)

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

我們來舉個例子:

console.log("1")
setTimeout(function print(){
   console.log("a") 
}, 0);
console.log("2")

這段代碼的執行過程,下方配圖:

  1. 創建全局上下文
  2. 創建console.log上下文,輸出 1 銷燬console.log上下文
  3. 創建setTimeout函數上下文,通知web api中的計時線程,計時0秒後執行print()函數。此時執行棧銷燬setTimeout函數上下文。web api計時0秒後,將延遲事件加入宏隊列
  4. 創建console.log上下文,輸出 2 銷燬console.log上下文。銷燬全局上下文
  5. 此時執行棧爲空,執行事件隊列中的任務。創建print函數上下文,創建console.log上下文,輸出 a。銷燬console.log上下文,銷燬print函數上下文。
  6. JS執行完畢

在這裏插入圖片描述
再附一個含有Promise回調的超級噁心的事件循環的例子:

setTimeout(()=>{
    console.log(1)
    a()
},0)
const pro= new Promise((resolve)=>{
    console.log(2)
    resolve(3)
})
pro.then(res=>{
    console.log(res)
})
function a(){
    setTimeout(() => {
        console.log(4)
    }, 0);
    console.log(5)
}
a()
console.log(6)

按照上面的步驟,很容易就能判斷出來。輸出結果爲:

2 5 6 3 1 5 4 4

最後的最後再附一個表情包!
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章