瀏覽器/nodeJS 中的事件環工做原理

衆所周知,JS的最大特色之一即是單線程.這意味着JS中若是從上到下執行命令,若是前面的命令花時間太長,則會出現"假死"狀態,影響用戶體驗. 所以在瀏覽器/nodeJS中,經過webAPI等方式, 將這些長時間的js命令經過異步"分流"到其餘的線程(JS自己是單線程,可是瀏覽器和nodeJS是多線程), 等這些命令執行完成後經過回調函數"返回"JS中. 而這一套機制的實現 就是事件環(eventloop). 下面咱們就來仔細研究一下它的工做原理.html

瀏覽器中的事件環工做原理

首先 用一張圖來展現瀏覽器中的事件環: 前端

Alt text
瀏覽器中的事件環

從這張圖中咱們能夠看到其中有宏任務(MacroTask)和微任務(MicroTask)之分,咱們來講下這個宏任務與微任務。node

宏任務包括:react

  • setTimeout
  • setInterval 微任務包括:
  • Promise
  • MutaionObserver
  • Object.observe(已廢棄:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe)

在單次的迭代中,event loop首先檢查Macrotask隊列,若是有一個Macrotask等待執行,那麼執行該任務。當該任務執行完畢後(或者Macrotask隊列爲空),event loop繼續執行Microtask隊列。(V8 中 Microtask 默認是自動運行的)。web

講了這麼多理論, 先來一點代碼看看:chrome

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');
複製代碼

這段代碼的順序如何呢? 將代碼放入chrome執行, 咱們能夠獲得順序以下: 數據庫

Alt text
注意 該圖是chrome的結果 不一樣瀏覽器可能呈現不一樣結果

那麼咱們來分析一下爲何是按照這個順序出現的

咱們先看一看wiki中對宏任務和微任務的定義:編程

"Tasks(宏任務) are scheduled so the browser can get from its internals into JavaScript/DOM land and ensures these actions happen sequentially. Between tasks, the browser may render updates. Getting from a mouse click to an event callback requires scheduling a task, as does parsing HTML, and in the above example, setTimeout.數組

setTimeout waits for a given delay then schedules a new task for its callback. This is why setTimeout is logged after script end, as logging script end is part of the first task, and setTimeout is logged in a separate task. Right, we're almost through this, but I need you to stay strong for this next bit…promise

Microtasks(微任務) are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task. The microtask queue is processed after callbacks as long as no other JavaScript is mid-execution, and at the end of each task. Any additional microtasks queued during microtasks are added to the end of the queue and also processed. Microtasks include mutation observer callbacks, and as in the above example, promise callbacks.

Once a promise settles, or if it has already settled, it queues a microtask for its reactionary callbacks. This ensures promise callbacks are async even if the promise has already settled. So calling .then(yey, nay) against a settled promise immediately queues a microtask. This is why promise1 and promise2 are logged after script end, as the currently running script must finish before microtasks are handled. promise1 and promise2 are logged before setTimeout, as microtasks always happen before the next task."

根據以上的描述, 咱們一步一步的分析以前的代碼:

step1

程序執行到第一行 直接輸出 script start

step2

接下來 setTimeout進入宏任務列表中 以下圖所示:

step3

接下來 Promise進入微任務列表中 以下圖所示:

step4

而後程序直行至最後一行,輸出script End:

step5

而後 先執行微任務中的命令:

step6

then中的部分是直接執行 所以console中顯示promise1

step7

因爲promise的回調函數中返回'undefined'因而將下一個promise 進入到微任務中.

step8

下圖中的 promise then 和promise callback對應的都是第二個then的. 而promise2也在console中顯示.

step9

最終結果如step10所示:

step10

看完了這一題 是否是以爲事件環也沒有想象中那麼難呢? 那麼在這裏你們能夠再看看下一題做爲思考題. 限於文章篇幅所限,僅提供正確答案供你們參考 ^_^

首先, 咱們來一個html頁面:

<div class="outer">
  <div class="inner"></div>
</div>
複製代碼

獲得以下的一個大方塊套小方塊的html頁面:

若是該html頁面的JS以下所示,那麼我點擊內部的小方塊,會獲得怎樣的結果呢?

// Let's get hold of those elements var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener… function onClick() { console.log('click'); setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } // …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
複製代碼

(使用chrome)正確答案:

click

promise

mutate

click

promise

mutate

timeout

timeout

node中的事件環工做原理

事件驅動

Node採用事件驅動的運行方式。在事件驅動的模型當中,每個IO工做被添加到事件隊列中,線程循環地處理隊列上的工做任務,當執行過程當中遇到來堵塞(讀取文件、查詢數據庫)時,線程不會停下來等待結果,而是留下一個處理結果的回調函數,轉而繼續執行隊列中的下一個任務。這個傳遞到隊列中的回調函數在堵塞任務運行結束後才被線程調用。

Node Async IO 這一套實現開始於Node開始啓動的進程,在這個進程中Node會建立一個循環,每次循環運行就是一個Tick週期,每一個Tick週期中會從事件隊列查看是否有事件須要處理,若是有就取出事件並執行相關的回調函數。事件隊列事件所有執行完畢,node應用就會終止。Node對於堵塞IO的處理在幕後使用線程池來確保工做的執行。Node從池中取得一個線程來執行復雜任務,而不佔用主循環線程。這樣就防止堵塞IO佔用空閒資源。當堵塞任務執行完畢經過添加到事件隊列中的回調函數來處理接下來的工做。

固然這麼華麗的運行機制就能解決前面說的兩個弊端。node基於事件的工做調度能很天然地將主要的調度工做限制到了一個線程,應用能很高效地處理多任務。程序每一時刻也只需管理一個工做中的任務。當必須處理堵塞IO時,經過將這個部分的IO控制權交給池中的線程,能最小地影響到應用處理事件,快速地反應web請求。 固然對機器方便的事情對於寫代碼的人來講就須要更當心地劃分業務邏輯,咱們須要將工做劃分爲合理大小的任務來適配事件模型這一套機制。

事件隊列調度

Node能夠經過傳遞迴調函數將任務添加到事件隊列中,這種異步的調度能夠經過5種方式來實現這個目標:異步堵塞IO庫(db處理、fs處理),Node內置的事件和事件監聽器(http、server的一些預約義事件),開發者自定義的事件和監聽器、定時器以及Node全局對象process的.nextTick()API。

異步堵塞IO庫

其IO庫提供的API有Node自帶的Module(好比fs)和數據庫驅動API,好比mongoose的.save(doc, callback)就是將繁重的數據庫Insert操做以及回調函數交給子線程來操做,主線程只負責任務的調度。當MongoDB返回給Node操做結果後,回調函數纔開始執行。

Dtree.create(frontData, function (err, dtree) {
      if (err) {
            console.log('Error: createDTree: DB failed to create due to ', err);
            res.send({'success': false, 'err': err});
      } else {
            console.log('Info: createDTree: DB created successfully dtree = ', dtree);
            res.send({'success': true, 'created_id': dtree._id.toHexString()});
      }
});
複製代碼

好比這段處理Dtree存儲的回調函數只有當事件隊列中的接收到來自堵塞IO處理線程的執行完畢纔會被執行。

Node內置的事件和事件監聽器

Node原生的模塊都預約義來一些事件,好比NET模塊的一套服務狀態事件。當Net中的Socket檢測到close就會調用放置在事件循環中的回調函數,下例中就是將sockets數組中刪除相應的socket鏈接對象。

socket.on('close', function(){
  console.log('connection closed');
  var index = sockets.indexOf(socket);
  //服務器端斷開相應鏈接
  sockets.splice(index, 1);
});
複製代碼

開發者自定義的事件

Node自身和不少模塊都支持開發者自定義事件和處理持戟處理函數,固然既然是自定義,那麼觸發事件也是顯性地須要開發者。在Socket.io編程中就有很好的例子,開發者能夠自定義消息事件來處理端對端的交互。

//socket監聽自定義的事件消息
socket.on('chatMessage', function(message){
  message.type = 'message';
  message.created = Date.now();
  message.username = socket.request.user.username;
  console.log(message);
  //同時也能夠像對方發出事件消息
  io.emit('chatMessage', message);
});
複製代碼

計時器(Timers)

Node使用前端一致的Timeout和Interval計時器,他們的區別在Timeout是延時執行,Interval是間隔一段事件執行。值得注意的是這組函數其實不屬於JS語言標準,他們只是擴展。在瀏覽器中,他們屬於BOM,即它的確切定義爲:window.setTimeout和window.setInterval;與window.alert, window.open等函數處於同一層次。Node把這組函數放置於全局範圍中。

除了這兩個函數,Node還添加Immediate計時器,setImmediate()函數是沒有事件參數的,在事件隊列中的當前任務執行結束後執行,而且優先級比Timeout、Interbal高。

計時器的問題在於它在事件循環中並不是精確的執行回調函數。《深刻淺出Node.js》舉了一個例子:當經過setTimeout()設定一個任務在10毫秒後執行,可是若是在9毫秒後,有一個任務佔用了5毫秒的CPU,再次燉老定時器執行時,事件就已通過期了。

Node全局對象process的.nextTick()API

這個延時執行函數函數是在添加任務到隊列的開頭,下一次Tick週期開始時就執行,也就是在其餘任務前調度。

nextTick的優先級是高於immediate的。而且每輪循環,nextTick中的回調函數所有都會執行完,而Immediate只會執行一個回調函數。這裏有得說明每一個Tick過程當中,判斷事件循環中是否有事件要處理的觀察者。在Node的底層libuv,事件循環是一個典型的生產者/消費者模型。異步IO、網絡請求是事件的生產者,回調函數是事件的消費者,而觀察者則是在中間將傳遞過來的事件暫存起來。回調函數的idle觀察者在每輪事件循環開始被檢查,而check觀察者後於idle觀察者檢查,二者之間被檢查的就是IO操做的觀察者。

事件驅動與高性能服務器

前面大體介紹了Node的事件驅動模型,事件驅動的實質就是主循環線程+事件觸發的方式來運行程序。Node的異步IO成功地使得IO操做與CPU操做分離成爲一套高性能平臺,既能夠像Nginx同樣構建服務器平臺,也能夠處理具體的業務。雖然Node沒有Nginx在Web服務器方面那麼專業,但不錯的性能和更多的使用場景使得在實際開發中可以達到優異的性能。這一切也都歸功與異步IO實現的核心——事件循環。在實際的項目中,咱們能夠結合不一樣工具的優勢達到應用的最優性能。

相關文章
相關標籤/搜索