首頁 > 軟體

深入瞭解Javascript的事件迴圈機制

2022-09-06 18:04:53

單執行緒的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事件迴圈機制的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com