熟悉javascript的朋友應該都使用過事件,好比鼠標的移動,鼠標的點擊,鍵盤的輸入等等。咱們在javascript中監聽這些事件,從而觸發相應的處理。javascript
一樣的nodejs中也有事件,而且還有一個專門的events模塊來進行專門的處理。java
同時事件和事件循環也是nodejs構建異步IO的很是重要的概念。node
今天咱們來詳細瞭解一下。web
nodejs爲事件提供了一個專門的模塊:lib/events.js。promise
還記得咱們在講使用nodejs構建web服務器嗎?瀏覽器
const server = http.createServer((req, res) => { res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end('welcome to www.flydean.com\n') })
這裏,每一個請求都會觸發request事件。服務器
nodejs的核心API是基於異步事件驅動來進行架構的,因此nodejs中有很是多的事件。多線程
好比:net.Server 會在每次有新鏈接時觸發事件,fs.ReadStream 會在打開文件時觸發事件,stream會在數據可讀時觸發事件。架構
咱們看一下怎麼來構建一個nodejs的事件:異步
const EventEmitter = require('events') const eventEmitter = new EventEmitter()
events經常使用的方法有兩個,分別是on和emit。
on用來監聽事件,emit用來觸發事件。
eventEmitter.on('fire', () => { console.log('開火') }) eventEmitter.emit('fire')
emit還能夠帶參數,咱們看下一個參數的狀況:
eventEmitter.on('fire', who => { console.log(`開火 ${who}`) }) eventEmitter.emit('fire', '美帝')
再看看兩個參數的狀況:
eventEmitter.on('fire', (who, when) => { console.log(`開火 ${who} ${when}`) }) eventEmitter.emit('fire', '川建國','now')
默認狀況下,EventEmitter以註冊的順序同步地調用全部監聽器。這樣能夠確保事件的正確排序,並有助於避免競態條件和邏輯錯誤。
若是須要異步執行,則可使用setImmediate() 或者 process.nextTick()來切換到異步執行模式。
eventEmitter.on('fire', (who, when) => { setImmediate(() => { console.log(`開火 ${who} ${when}`); }); }) eventEmitter.emit('fire', '川建國','now')
除此以外,events還支持其餘幾個方法:
once(): 添加單次監聽器
removeListener() / off(): 從事件中移除事件監聽器
removeAllListeners(): 移除事件的全部監聽器
咱們知道nodejs的代碼是運行在單線程環境中的,每次只會去處理一件事情。
這一種處理方式,避免了多線程環境的數據同步的問題,大大的提高了處理效率。
所謂事件循環,就是指處理器在一個程序週期中,處理完這個週期的事件以後,會進入下一個事件週期,處理下一個事件週期的事情,這樣一個週期一個週期的循環。
若是咱們在事件處理過程當中,某個事件的處理髮生了阻塞,則會影響其餘的事件的執行,因此咱們能夠看到在JS中,幾乎全部的IO都是非阻塞的。這也是爲何javascript中有這麼多回調的緣由。
咱們看一個簡單的事件循環的例子:
const action2 = () => console.log('action2') const action3 = () => console.log('action3') const action1 = () => { console.log('action1') action2() action3() } action1()
上面的代碼輸出:
action1 action2 action3
咱們知道函數間的調用是經過棧來實現的,上面的例子中,咱們的調用順序也是經過棧來實現的。
但並非函數中全部的方法都會入棧,還有一些方法會被放入消息隊列。
咱們再舉一個例子:
const action2 = () => console.log('action2') const action3 = () => console.log('action3') const action1 = () => { console.log('action1') setTimeout(action2, 0) action3() } action1()
上面的代碼運行結果:
action1 action3 action2
結果不同了。這是由於settimeout觸發了定時器,當定時器到期的時候,回調函數會被放入消息隊列中等待被處理,而不是放入棧中。
事件循環會優先處理棧中的事件,只有棧中沒有任何數據的時候,纔會去轉而消費消息隊列中的事件。
雖然上面例子中setTimeout的timeout時間是0,可是仍是要等到action3執行完畢才能執行。
注意,setTimeout中的timeout並非在當前線程進行等待的,它是由瀏覽器或者其餘JS執行環境來調用的。
ES6中的Promise引入了做業隊列的概念,使用做業隊列將會盡快地執行異步函數的結果,而不是放在調用堆棧的末尾。
舉個例子:
const action2 = () => console.log('action2') const action3 = () => console.log('action3') const action1 = () => { console.log('action1') setTimeout(action2, 0) new Promise((resolve, reject) => resolve('應該在action3以後、action2以前') ).then(resolve => console.log(resolve)) action3() } action1()
輸出結果:
action1 action3 應該在action3以後、action2以前 action2
這是由於,在當前函數結束以前 resolve 的 Promise 會在當前函數以後被當即執行。
也就是說先執行棧,再執行做業隊列,最後執行消息隊列。
先給你們一個定義叫作tick,一個tick就是指一個事件週期。而process.nextTick()就是指在下一個事件循環tick開始以前,調用這個函數:
process.nextTick(() => { console.log('i am the next tick'); })
因此nextTick必定要比消息隊列的setTimeout要快。
nodejs提供了一個setImmediate方法,來儘快的執行代碼。
setImmediate(() => { console.log('I am immediate!'); })
setImmediate中的函數會在事件循環的下一個迭代中執行。
setImmediate() 和 setTimeout(() => {}, 0)的功能基本上是相似的。它們都會在事件循環的下一個迭代中運行。
若是想要定時執行某些回調函數,則須要用到setInterval。
setInterval(() => { console.log('每隔2秒執行一次'); }, 2000)
要清除上面的定時任務,可使用clearInterval:
const id = setInterval(() => { console.log('每隔2秒執行一次'); }, 2000) clearInterval(id)
注意,setInterval是每隔n毫秒啓動一個函數,無論該函數是否執行完畢。
若是一個函數執行時間太長,就會致使下一個函數同時執行的狀況,怎麼解決這個問題呢?
咱們能夠考慮在回調函數內部再次調用setTimeout,這樣造成遞歸的setTimeout調用:
const myFunction = () => { console.log('作完後,隔2s再次執行!'); setTimeout(myFunction, 2000) } setTimeout(myFunction, 2000)
本文做者:flydean程序那些事
本文連接:http://www.flydean.com/nodejs-event/
本文來源:flydean的博客
歡迎關注個人公衆號:「程序那些事」最通俗的解讀,最深入的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!