Js事件循環(huán)(Event Loop)機(jī)制

前言

Event Loop是計(jì)算機(jī)系統(tǒng)的一種運(yùn)行機(jī)制,是個(gè)很重要的概念。而Javascript用這種機(jī)制來(lái)解決單線程運(yùn)行帶來(lái)的問(wèn)題。理解很熟悉將會(huì)有利于我們更容易理解Vue的異步事件。

JavaScript是單線程的

1、什么是單線程?

單線程在程序執(zhí)行時(shí),所走的程序路徑按照連續(xù)順序排下來(lái),前面的必須處理好,后面的才會(huì)執(zhí)行。簡(jiǎn)單來(lái)說(shuō),即同一時(shí)間只能做一件事件。

2、Js為什么是單線程?

Js是一種運(yùn)行在網(wǎng)頁(yè)的簡(jiǎn)單的腳本語(yǔ)言,由于設(shè)計(jì)的初衷是作為瀏覽器腳本語(yǔ)言,用于與用戶互動(dòng),以及操作DOM。這決定它是單線程的。

3、單線程帶來(lái)的問(wèn)題?

單線程就意味著,所有任務(wù)都需要排隊(duì),前一個(gè)任務(wù)結(jié)束,才會(huì)執(zhí)行后一個(gè)任務(wù)。如果前一個(gè)任務(wù)耗時(shí)很長(zhǎng),后一個(gè)任務(wù)就需要一直等著。這就會(huì)導(dǎo)致IO操作(耗時(shí)但cpu閑置)時(shí)造成性能浪費(fèi)的問(wèn)題。

4、如何解決單線程的性能問(wèn)題?

采用異步可以解決。主線程完全可以不管IO操作,暫時(shí)掛起處于等待中的任務(wù),先運(yùn)行排在后面的任務(wù)。等到IO操作返回了結(jié)果,再回過(guò)頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去。于是,所有任務(wù)可以分成兩種,一種是同步任務(wù),另一種是異步任務(wù)。

執(zhí)行棧

當(dāng)Javascript代碼執(zhí)行的時(shí)候會(huì)將不同的變量存于內(nèi)存中的不同位置:堆(heap)和棧(stack)中來(lái)加以區(qū)分。其中,堆里存放著一些對(duì)象。而棧中則存放著一些基礎(chǔ)類型變量以及對(duì)象的指針。但是我們這里說(shuō)的執(zhí)行棧和上面這個(gè)棧的意義卻有些不同。

js 在執(zhí)行可執(zhí)行的腳本時(shí),會(huì)經(jīng)過(guò)以下步驟:

  1. 首先會(huì)創(chuàng)建一個(gè)全局可執(zhí)行上下文globalContext,每當(dāng)執(zhí)行到一個(gè)函數(shù)調(diào)用時(shí)都會(huì)創(chuàng)建一個(gè)可執(zhí)行上下文(execution context)EC。
  2. 可執(zhí)行程序可能會(huì)存在很多函數(shù)調(diào)用,那么就會(huì)創(chuàng)建很多EC,所以 JavaScript 引擎創(chuàng)建了執(zhí)行上下文棧(Execution context stack,ECS)來(lái)管理執(zhí)行上下文。
  3. 當(dāng)函數(shù)調(diào)用完成,Js會(huì)退出這個(gè)執(zhí)行環(huán)境并把這個(gè)執(zhí)行環(huán)境銷毀,回到上一個(gè)方法的執(zhí)行環(huán)境。 這個(gè)過(guò)程反復(fù)進(jìn)行,直到執(zhí)行棧中的代碼全部執(zhí)行完畢。

image

實(shí)例

function fun3() {
    console.log('fun3')
}
function fun2() {
    fun3();
}
function fun1() {
    fun2();
}
fun1();

當(dāng)執(zhí)行一個(gè)函數(shù)的時(shí)候,就會(huì)創(chuàng)建一個(gè)執(zhí)行上下文,并且壓入執(zhí)行上下文棧,當(dāng)函數(shù)執(zhí)行完畢的時(shí)候,就會(huì)將函數(shù)的執(zhí)行上下文從棧中彈出。知道了這樣的工作原理,讓我們來(lái)看看如何處理上面這段代碼:

1.執(zhí)行全局代碼,創(chuàng)建全局執(zhí)行上下文,全局上下文被壓入執(zhí)行上下文棧

ECStack = [
    globalContext
];
  1. 全局上下文初始化
   globalContext = {
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }
  1. 初始化的同時(shí),fun1函數(shù)被創(chuàng)建,保存作用域鏈到函數(shù)的內(nèi)部屬性[[scope]]
 fun1.[[scope]] = [
      globalContext.VO
    ];
  1. 執(zhí)行 fun1 函數(shù),創(chuàng)建fun1函數(shù)執(zhí)行上下文,fun1函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧
 ECStack = [
        fun1,
        globalContext
    ];
  1. fun1函數(shù)執(zhí)行上下文初始化:

    1.復(fù)制函數(shù) [[scope]] 屬性創(chuàng)建作用域鏈。

    2.用 arguments 創(chuàng)建活動(dòng)對(duì)象。

    3.初始化活動(dòng)對(duì)象,即加入形參、函數(shù)聲明、變量聲明。

    4.將活動(dòng)對(duì)象壓入fun1 作用域鏈頂端。
    同時(shí) f 函數(shù)被創(chuàng)建,保存作用域鏈到 f 函數(shù)的內(nèi)部屬性[[scope]]

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

事件循環(huán)(Event Loop)

JavaScript內(nèi)存模型

在了解事件循環(huán)之前,先要弄明白Js的內(nèi)存模型,這有助于更好的理解事件循環(huán)。

  • 調(diào)用棧(Call Stack):用于主線程任務(wù)的執(zhí)行。
  • 堆(Heap):用于存放非結(jié)構(gòu)數(shù)據(jù),如程序分配的變量和對(duì)象。
  • 任務(wù)隊(duì)列(Queue): 用于存放異步任務(wù)。

Js異步執(zhí)行的運(yùn)行機(jī)制

  1. 所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧。
  2. 主線程之外,還存在一個(gè)任務(wù)隊(duì)列。只要異步任務(wù)有了運(yùn)行結(jié)果,就在任務(wù)隊(duì)列之中放置一個(gè)事件。
  3. 一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列,看看里面有哪些事件。那些對(duì)應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進(jìn)入執(zhí)行棧,開始執(zhí)行。
  4. 主線程不斷重復(fù)上面的第三步。

任務(wù)

異步任務(wù)存放在任務(wù)隊(duì)列里,異步任務(wù)分為 宏任務(wù)(macrotask)與微任務(wù)(microtask),不同的API注冊(cè)的任務(wù)會(huì)依次進(jìn)入自身對(duì)應(yīng)的隊(duì)列中,然后等待Event Loop將它們依次壓入執(zhí)行棧中執(zhí)行。

宏任務(wù)主要包含:

  • script(整體代碼)
  • setTimeout
  • setInterval
  • I/O、UI交互事件
  • setImmediate(Node.js 環(huán)境)

微任務(wù)主要包含:

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

我們的JavaScript的執(zhí)行過(guò)程是單線程的,所有的任務(wù)可以看做存放在兩個(gè)隊(duì)列中——執(zhí)行隊(duì)列和事件隊(duì)列。

執(zhí)行隊(duì)列里面是所有同步代碼的任務(wù),事件隊(duì)列里面是所有異步代碼的宏任務(wù),而我們的微任務(wù),是處在兩個(gè)隊(duì)列之間。

當(dāng)JavaScript執(zhí)行時(shí),優(yōu)先執(zhí)行完所有同步代碼,遇到對(duì)應(yīng)的異步代碼,就會(huì)根據(jù)其任務(wù)類型存到對(duì)應(yīng)隊(duì)列(宏任務(wù)放入事件隊(duì)列,微任務(wù)放入執(zhí)行隊(duì)列之后,事件隊(duì)列之前);當(dāng)執(zhí)行完同步代碼之后,就會(huì)執(zhí)行位于執(zhí)行隊(duì)列和事件隊(duì)列之間的微任務(wù),然后再執(zhí)行事件隊(duì)列中的宏任務(wù)。

實(shí)例

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 任務(wù)先運(yùn)行。首先遇到Promise實(shí)例,構(gòu)造函數(shù)首先執(zhí)行,所以首先輸出了 4。此時(shí)microtask 的任務(wù)有 t2t1
  2. script 任務(wù)繼續(xù)運(yùn)行,輸出 3。至此,第一個(gè)宏任務(wù)執(zhí)行完成。
  3. 執(zhí)行所有的微任務(wù),先后取出 t2t1,分別輸出 21
  4. 代碼執(zhí)行完畢

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

事件循環(huán)

主線程從任務(wù)隊(duì)列中讀取事件,這個(gè)過(guò)程是循環(huán)不斷的,所以整個(gè)的這種運(yùn)行機(jī)制又稱為Event Loop(事件循環(huán))。

image

從上圖我們可以看出:

  • 主線程運(yùn)行的時(shí)候,產(chǎn)生堆(heap)和棧(stack)。
  • 棧中的代碼調(diào)用各種外部API,它們?cè)?任務(wù)隊(duì)列"中加入各種事件(click,load,done)。
  • 棧中的代碼執(zhí)行完畢,主線程就會(huì)去讀取任務(wù)隊(duì)列,依次執(zhí)行那些事件所對(duì)應(yīng)的回調(diào)函數(shù)。

小結(jié)

事件循環(huán)其實(shí)并不難,多查閱資料,多看看相關(guān)例子就ok。希望一知半解的童鞋抓緊學(xué)習(xí)。

相關(guān)文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容