深入瞭解Javascript的事件循環機制

單線程的Javascript

JavaScript是一種單線程語言,它主要用來與用戶互動,以及操作DOM。多線程需要共享資源、且有可能修改彼此的運行結果,且存在上下文切換。

在 JS 運行的時候可能會阻止 UI 渲染,這說明兩個線程是互斥的。這是因為 JS 可以修改 DOM,如果在 JS 執行的時候 UI 線程還在工作,就可能導致不能安全的渲染 UI。

JS 是單線程運行的,可以達到節省內存,節約上下文切換時間。

為瞭利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。

單線程的同步等待極大影響效率,任務不得不一個一個等待執行,對於網頁應用是無法接受的。所以Javascript使用事件循環機制來解決異步任務的問題。

同步 vs 異步 宏任務 vs 微任務

首先瞭解下同步和異步的區別:

  • 同步:在一個函數返回的時候,調用者就能夠得到預期結果。
  • 同步任務:在主線程上排隊執行的任務,隻有前一個任務執行完畢,才能執行後一個任務。
  • 異步:在函數返回的時候,調用者還不能夠得到預期結果,而是需要在將來通過一定的手段得到。
  • 異步任務:不進入主線程、而放在"任務隊列"中的任務,若有多個異步任務,則需排隊等待進入主線程執行棧中被執行。

任務隊列其實不止一種,根據任務種類的不同,可以分為微任務(micro task)隊列和宏任務(macro task)隊列。常見的任務如下:

  • 宏任務:script(整體代碼)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate;需要特定的異步線程去執行,有明確的異步任務去執行,有回調。
  • 微任務:Promise、MutaionObserver、process.nextTick(Node.js 環境,會先於其他微任務執行);不需要特定的異步線程去執行,沒有明確的異步任務去執行,隻有回調。

一次 Eventloop 循環會處理一個宏任務和所有這次循環中產生的微任務。 執行順序如下圖:

第一個例子:

var req = new XMLHttpRequest();
req.open('GET', url);    
req.onload = function (){};    
req.onerror = function (){};    
req.send();
//等同於
var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};    
req.onerror = function (){};

上面代碼中的req.send方法是Ajax操作向服務器發送數據,它是一個異步任務,意味著隻有當前腳本的所有代碼執行完,系統才會去讀取"任務隊列"。指定回調函數的部分(onload和onerror),在send()方法的前面或後面無關緊要,因為它們屬於執行棧的一部分,系統總是執行完它們,才會去讀取"任務隊列"。

第二個例子:

console.log('1 第一次循環 開始執行');
setTimeout(function () {
    console.log('2 第二次循環 開始執行');
    new Promise(function (resolve) {
        console.log('3 第二次循環 宏任務結束');
        resolve();
    }).then(function () {
        console.log('4 第二次循環 微任務執行')
    })
}, 0)
new Promise(function (resolve) {
    console.log('5 第一次循環 宏任務結束');
    resolve();
}).then(function () {
    console.log('6 第一次循環 微任務執行')
})

setTimeout(function () {
    console.log('7 第三次循環 開始執行');

    new Promise(function (resolve) {
        console.log('8 第三次循環 宏任務結束');
        resolve();
    }).then(function () {
        console.log('9 第三次循環 微任務執行')
    })
}, 0)

/*
結果
1 第一次循環 開始執行
5 第一次循環 宏任務結束
6 第一次循環 微任務執行
2 第二次循環 開始執行
3 第二次循環 宏任務結束
4 第二次循環 微任務執行
7 第三次循環 開始執行
8 第三次循環 宏任務結束
9 第三次循環 微任務執行
*/

定時器

定時器功能主要由setTimeout()和setInterval()這兩個函數來完成,它們的內部運行機制完全一樣,區別在於前者指定的代碼是一次性執行,後者則為反復執行。

如果將setTimeout()的第二個參數設為0,就表示當前代碼執行完(執行棧清空)以後,立即執行(0毫秒間隔)指定的回調函數。主線程盡可能早得執行,但是沒有辦法保證回調函數一定會在setTimeout()指定的時間執行,因為必須等到當前代碼(執行棧)執行完,主線程才會去執行它指定的回調函數。所以單線程無法實現真正的異步,因為還是存在阻塞。

HTML5標準規定瞭setTimeout()的第二個參數的最小值(最短間隔),不得低於4毫秒,如果低於這個值,就會自動增加。

在此之前,老版本的瀏覽器都將最短間隔設為10毫秒。

另外,對於那些DOM的變動(尤其是涉及頁面重新渲染的部分),通常不會立即執行,而是每16毫秒執行一次。

這時使用requestAnimationFrame()的效果要好於setTimeout()。

在Node.js環境下,還提供瞭另外兩個方法:

  • process.nextTick方法可以在當前"執行棧"的尾部,下一次Event Loop之前,觸發回調函數。也就是說,它指定的任務總是在本次"事件循環"觸發,發生在所有異步任務之前,同時也是在所有微任務之前執行。
  • setImmediate方法則是在當前"任務隊列"的尾部添加事件,也就是說,它指定的任務總是在之後的Event Loop執行,這與setTimeout(fn, 0)很像。

多個process.nextTick​語句總是在當前"執行棧"一次執行完,多個setImmediate則可能需要多次loop才能執行完。

To Be Continued

Node.js使用V8作為js的解析引擎,而I/O處理方面使用瞭自己設計的libuv,libuv是一個基於事件驅動的跨平臺抽象層,封裝瞭不同操作系統一些底層特性,對外提供統一的API,事件循環機制也是它裡面的實現的。(在Python中,uvloop,一個完整的asyncio事件循環的替代品,也是建立在libuv基礎之上,是由Cython編寫而成。)這個機制和瀏覽器中Javascript的事件循環機制是不太一樣的。

以上就是深入瞭解Javascript的事件循環機制的詳細內容,更多關於Javascript事件循環機制的資料請關註WalkonNet其它相關文章!

推薦閱讀: