Js事件循環(Event Loop)機制

前言

Event Loop是計算機系統的一種運行機制,是個很重要的概念。而Javascript用這種機制來解決單線程運行帶來的問題。理解很熟悉將會有利於咱們更容易理解Vue的異步事件。html

更多優質文章請猛戳GitHub博客,歡迎帥哥美女前來Star!!!git

JavaScript是單線程的

一、什麼是單線程?

單線程在程序執行時,所走的程序路徑按照連續順序排下來,前面的必須處理好,後面的纔會執行。簡單來講,即同一時間只能作一件事件。github

二、Js爲何是單線程?

Js是一種運行在網頁的簡單的腳本語言,因爲設計的初衷是做爲瀏覽器腳本語言,用於與用戶互動,以及操做DOM。這決定它是單線程的。瀏覽器

三、單線程帶來的問題?

單線程就意味着,全部任務都須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就須要一直等着。這就會致使IO操做(耗時但cpu閒置)時形成性能浪費的問題。bash

四、如何解決單線程的性能問題?

採用異步能夠解決。主線程徹底能夠無論IO操做,暫時掛起處於等待中的任務,先運行排在後面的任務。等到IO操做返回告終果,再回過頭,把掛起的任務繼續執行下去。因而,全部任務能夠分紅兩種,一種是同步任務,另外一種是異步任務。異步

執行棧

Javascript代碼執行的時候會將不一樣的變量存於內存中的不一樣位置:堆(heap)和棧(stack)中來加以區分。其中,堆裏存放着一些對象。而棧中則存放着一些基礎類型變量以及對象的指針。可是咱們這裏說的執行棧和上面這個棧的意義卻有些不一樣。函數

js 在執行可執行的腳本時,會通過如下步驟:oop

  1. 首先會建立一個全局可執行上下文globalContext,每當執行到一個函數調用時都會建立一個可執行上下文(execution context)EC
  2. 可執行程序可能會存在不少函數調用,那麼就會建立不少EC,因此 JavaScript 引擎建立了執行上下文棧(Execution context stack,ECS)來管理執行上下文。
  3. 當函數調用完成,Js會退出這個執行環境並把這個執行環境銷燬,回到上一個方法的執行環境。 這個過程反覆進行,直到執行棧中的代碼所有執行完畢。

實例

function fun3() {
    console.log('fun3')
}
function fun2() {
    fun3();
}
function fun1() {
    fun2();
}
fun1();
複製代碼

當執行一個函數的時候,就會建立一個執行上下文,而且壓入執行上下文棧,當函數執行完畢的時候,就會將函數的執行上下文從棧中彈出。知道了這樣的工做原理,讓咱們來看看如何處理上面這段代碼:性能

1.執行全局代碼,建立全局執行上下文,全局上下文被壓入執行上下文棧學習

ECStack = [
    globalContext
];
複製代碼
  1. 全局上下文初始化
globalContext = {
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }
複製代碼
  1. 初始化的同時,fun1函數被建立,保存做用域鏈到函數的內部屬性[[scope]]
fun1.[[scope]] = [
      globalContext.VO
    ];
複製代碼
  1. 執行 fun1 函數,建立fun1函數執行上下文,fun1函數執行上下文被壓入執行上下文棧
ECStack = [
        fun1,
        globalContext
    ];
複製代碼
  1. fun1函數執行上下文初始化:

    1.複製函數 [[scope]] 屬性建立做用域鏈。

    2.用 arguments 建立活動對象。

    3.初始化活動對象,即加入形參、函數聲明、變量聲明。

    4.將活動對象壓入fun1 做用域鏈頂端。 同時 f 函數被建立,保存做用域鏈到 f 函數的內部屬性[[scope]]

checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
複製代碼
  1. 執行 fun2() 函數,重複步驟4。
  2. 最終造成這樣的執行棧:
ECStack = [
        fun3
        fun2,
        fun1,
        globalContext
    ];
複製代碼
  1. fun3執行完畢,從執行棧中彈出...一直到fun1

事件循環(Event Loop)

JavaScript內存模型

在瞭解事件循環以前,先要弄明白Js的內存模型,這有助於更好的理解事件循環。

  • 調用棧(Call Stack):用於主線程任務的執行。
  • 堆(Heap):用於存放非結構數據,如程序分配的變量和對象。
  • 任務隊列(Queue): 用於存放異步任務。

Js異步執行的運行機制

  1. 全部同步任務都在主線程上執行,造成一個執行棧。
  2. 主線程以外,還存在一個任務隊列。只要異步任務有了運行結果,就在任務隊列之中放置一個事件。
  3. 一旦執行棧中的全部同步任務執行完畢,系統就會讀取任務隊列,看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。
  4. 主線程不斷重複上面的第三步。

任務

異步任務存放在任務隊列裏,異步任務分爲 宏任務(macrotask)與微任務(microtask),不一樣的API註冊的任務會依次進入自身對應的隊列中,而後等待Event Loop將它們依次壓入執行棧中執行。

宏任務主要包含:

  • script(總體代碼)
  • setTimeout
  • setInterval
  • I/OUI交互事件
  • setImmediate(Node.js 環境)

微任務主要包含:

  • Promise
  • MutaionObserver
  • process.nextTick(Node.js 環境)

咱們的JavaScript的執行過程是單線程的,全部的任務能夠看作存放在兩個隊列中——執行隊列和事件隊列。

執行隊列裏面是全部同步代碼的任務,事件隊列裏面是全部異步代碼的宏任務,而咱們的微任務,是處在兩個隊列之間。

JavaScript執行時,優先執行完全部同步代碼,遇到對應的異步代碼,就會根據其任務類型存到對應隊列(宏任務放入事件隊列,微任務放入執行隊列以後,事件隊列以前);當執行完同步代碼以後,就會執行位於執行隊列和事件隊列之間的微任務,而後再執行事件隊列中的宏任務。

實例

new Promise(resolve => {
    resolve(1);
    
    Promise.resolve().then(() => {
    	// t2
    	console.log(2)
    });
    console.log(4)
}).then(t => {
	// t1
	console.log(t)
});
console.log(3);
複製代碼

這段代碼的流程大體以下:

  1. script 任務先運行。首先遇到Promise實例,構造函數首先執行,因此首先輸出了 4。此時microtask 的任務有 t2t1
  2. script 任務繼續運行,輸出 3。至此,第一個宏任務執行完成。
  3. 執行全部的微任務,前後取出 t2t1,分別輸出 21
  4. 代碼執行完畢

綜上,上述代碼的輸出是:4321

事件循環

主線程從任務隊列中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)。

從上圖咱們能夠看出:

  • 主線程運行的時候,產生堆(heap)和棧(stack)。
  • 棧中的代碼調用各類外部API,它們在"任務隊列"中加入各類事件(click,load,done)。
  • 棧中的代碼執行完畢,主線程就會去讀取任務隊列,依次執行那些事件所對應的回調函數。

小結

事件循環其實並不難,多查閱資料,多看看相關例子就ok。但願只知其一;不知其二的童鞋抓緊學習。

相關文章

相關文章
相關標籤/搜索