簡介
熟悉javascript的朋友應該都使用過事件,比如滑鼠的移動,滑鼠的點選,鍵盤的輸入等等。我們在javascript中監聽這些事件,進而觸發相應的處理。
同樣的nodejs中也有事件,并且還有一個專門的events子產品來進行專門的處理。
同時事件和事件循環也是nodejs建構異步IO的非常重要的概念。
今天我們來詳細了解一下。
事件
nodejs為事件提供了一個專門的子產品:lib/events.js。
還記得我們在講使用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執行環境來調用的。
作業隊列和promise
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 會在目前函數之後被立即執行。
也就是說先執行棧,再執行作業隊列,最後執行消息隊列。
process.nextTick()
先給大家一個定義叫做tick,一個tick就是指一個事件周期。而process.nextTick()就是指在下一個事件循環tick開始之前,調用這個函數:
process.nextTick(() => {
console.log('i am the next tick');
})
是以nextTick一定要比消息隊列的setTimeout要快。
setImmediate()
nodejs提供了一個setImmediate方法,來盡快的執行代碼。
setImmediate(() => {
console.log('I am immediate!');
})
setImmediate中的函數會在事件循環的下一個疊代中執行。
setImmediate() 和 setTimeout(() => {}, 0)的功能基本上是類似的。它們都會在事件循環的下一個疊代中運作。
setInterval()
如果想要定時執行某些回調函數,則需要用到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的部落格
歡迎關注我的公衆号:「程式那些事」最通俗的解讀,最深刻的幹貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!