node.js事件events詳解
nodejs基於事件驅動
事件釋出,事件訂閱
- 大多數 Node.js 核心 API 構建於慣用的非同步事件驅動架構,其中某些型別的物件(又稱觸發器,Emitter)會觸發命名事件來呼叫函式(又稱監聽器,Listener)。同步函式,非同步觸發,通過回撥函式處理非同步。
例如,net.Server 會在每次有新連線時觸發事件,fs.ReadStream 會在檔案被開啟時觸發事件,stream會在資料可讀時觸發事件。
所有能觸發事件的物件都是 EventEmitter 類的例項。 這些物件有一個 eventEmitter.on() 函式,用於將一個或多個函式繫結到命名事件上。 事件的命名通常是駝峰式的字串。
當 EventEmitter 物件觸發一個事件時,所有繫結在該事件上的函式都會被同步地呼叫。
- 這是node.js文件的一個例子: 我的程式碼下載index.js檔案裡,通過node index執行
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('觸發事件');
});
myEmitter.emit('event' ); //這個就會觸發myEmitter.on裡的回撥函式
- 實際上這個例子不太好,我再處理一下:
放在以前,我們處理"非同步"都是這樣做的(需要的話會傳一些引數啥的):
let fn = () => {
console.log("資料結果的處理");
}
setTimeout(() => {
console.log("得到資料");
fn()
}, 2000);
在純的js裡,雖然可以達到預期目標,但是層層巢狀的函式實在是太讓人頭疼,為了減少這種麻煩,node.js的events的事件監聽就能很好的解決。
- EventEmitter,釋出-訂閱模式:
const EventEmitter = require("events").EventEmitter
const myEmitter = new EventEmitter
let fn = () => {
console.log("資料結果的處理");
}
//為了好看設定的定時*********
setTimeout(() => {
console.log("1s");
}, 1000);
setTimeout(() => {
console.log("1.5s");
}, 1500);
setTimeout(() => {
console.log("2s");
}, 2000);
//************************
setTimeout(() => {
console.log("2.5s 得到資料");
myEmitter.emit("dataMake")
}, 2500);
myEmitter.on("dataMake",fn)
這樣,非同步觸發回撥函式將會變得更加好歸類、結構化。
用於物件
- 讓物件繼承事件的EventEmitter的原型:
const EventEmitter = require("events").EventEmitter
function FnObj(params) {
this.name = params;
}
FnObj.prototype.__proto__=EventEmitter.prototype
const obj = new FnObj("Lucy");
obj.on("ly",function() {
console.log(this.name);
//注意箭頭函式沒有this哦
})
setTimeout(() => {
obj.emit("ly")
}, 2000);
這就是node.js的事件,釋出-訂閱模式
events使用進階
非同步執行的處理函式
雖然對函式的處理變得更加結構有序,但是非同步執行總會發生一些資料競爭的情況,誰也不知道什麼時候資料能被真正的準備好供非同步函式使用。
或許有人會說我的處理函式在資料得到返回的函式裡顯示呼叫呢?然後現實開發中並不一定完全按照你的想法來執行,很多網站的資料並不能僅通過一個表單獲取,也不一定保證兩個資料的先後結果,在兩個資料集合得到時間不一致或不能保證一致的情況下,非同步執行就是個問題。
所以,處理好函式執行順序則是至關重要的:
- 第一步,搞清楚到底傳了什麼引數進去
傳引數和this到你的監聽器:
eventEmitter.emit() 方法可以傳任意數量的引數到監聽器函式。 當監聽器函式被呼叫時,this 關鍵詞會被指向監聽器所繫結的 EventEmitter 例項
不要用箭頭函式 不要用箭頭函式 不要用箭頭函式 重要的事情說三遍,箭頭函式沒有this,它根本指不到你的例項
const EventEmitter = require("events").EventEmitter
const myEmitter = new EventEmitter();
myEmitter.on('event', function (a, b) {
console.log(a, b);
console.log(this);
console.log(this === myEmitter);
});
myEmitter.emit('event', '引數a', '引數b');
貼心的我又簡單優化了node文件的列印結果,更加易讀
但是如果有些人就是倔強,我就要寫箭頭函式,我牛逼:
const EventEmitter = require("events").EventEmitter
const myEmitter = new EventEmitter();
myEmitter.on('event', (a, b)=> {
console.log(a, b);
console.log(this);
console.log(this === myEmitter);
});
myEmitter.emit('event', '引數a', '引數b');
對比上一次的結果:
const EventEmitter = require("events").EventEmitter
const myEmitter = new EventEmitter();
myEmitter.on('event', (a, b,othis)=> {
console.log(a, b);
console.log(othis);
console.log(othis === myEmitter);
});
myEmitter.emit('event', '引數a', '引數b', myEmitter);
非要用箭頭函式,但是這樣不很麻煩嗎?
同步還是非同步,無所謂
- 加上引數傳遞,一切都不是障礙了:
const EventEmitter = require("events").EventEmitter
const myEmitter = new EventEmitter();
let data = {}
myEmitter.on('event', function (data) {
setImmediate(() => {
//假如這裡有個數據結構化、圖形化的處理函式
console.log('非同步進行結果處理');
console.log(data.name, data.age, data.height,data.sex);
});
});
myEmitter.emit('event', data); //雖然我立馬就準備觸發資料處理函式
process.nextTick(() => {
console.log("同步的程式碼去獲取到了資料")
data.name = "小芳";
data.age = "18"
} )
process.nextTick(() => {
console.log("同步的程式碼去獲取到了資料")
data.height = "1米8"
})
Promise.resolve("女").then(res => {
console.log("非同步的then函式也去拿了一下資料")
data.sex = res
})
即便我先於獲取資料的函式呼叫了資料處理的函式,但是結合同步非同步函式,資料處理得到了最後的保障:
也有人可能會問,你這資料暴露在全域性這樣做有意義嗎?
回答:這裡是為了呼叫方便,你完全可以將資料請求封裝起來,讓他們在區域性執行,並將事件觸發也放在區域性作用域裡,引數傳遞只是事件釋出函式的基本功能,合理使用效果更佳!
監聽事件的行為
- 監聽事件的建立:
const EventEmitter = require("events").EventEmitter
const myEmitter = new EventEmitter();
myEmitter.on('newListener',function() {
console.log('有事件創建出來');
})
myEmitter.on('event', function () {
console.log('事件處理函式呼叫');
});
// myEmitter.emit('event')
//列印:有事件創建出來
如果我重新開啟事件觸發,那麼結果是這樣
- 事件解綁:
const EventEmitter = require("events").EventEmitter
const myEmitter = new EventEmitter();
function fn() {
console.log('事件處理函式呼叫');
}
myEmitter.on('newListener',function() {
console.log('有事件創建出來');
})
myEmitter.on('event', fn);
myEmitter.emit('event')
myEmitter.off('event', fn);
myEmitter.emit('event')
myEmitter.emit('event')
myEmitter.emit('event')
事件解綁是將回調函式與監聽的事件分離,如果想上面一樣直接把回撥函式寫在事件繫結函式裡是無法解綁,下面是錯誤的示範:
- 監聽事件的解綁:
const EventEmitter = require("events").EventEmitter
const myEmitter = new EventEmitter();
function fn() {
console.log('事件處理函式呼叫');
}
myEmitter.on('newListener',function() {
console.log('有事件創建出來');
})
myEmitter.on('removeListener', function () {
console.log('有事件被解綁');
})
myEmitter.on('event', fn);
myEmitter.emit('event')
myEmitter.off('event', fn);
myEmitter.emit('event')
myEmitter.emit('event')
myEmitter.emit('event')
- 或者只讓事件執行一次:
const EventEmitter = require("events").EventEmitter
const myEmitter = new EventEmitter();
function fn() {
console.log('事件處理函式呼叫');
}
myEmitter.on('newListener',function() {
console.log('有事件被建立1');
})
myEmitter.on('newListener',function() {
console.log('有事件被建立2');
})
myEmitter.on('removeListener', function () {
console.log('有事件被解綁');
})
myEmitter.once('event', fn);
console.log('1111111111111111111111');
myEmitter.emit('event')
console.log('2222222222222222222222');
myEmitter.emit('event')
myEmitter.emit('event')
myEmitter.emit('event')
兩個事件監聽器弄清楚流程: 事件新增監聽器1被建立,但是當時沒有監聽事件建立的監聽器; 事件新增監聽器2被建立,觸發了事件建立監聽器1,列印:事件被建立1; 事件移除監聽器被建立,觸發事件建立監聽器1,2,列印:事件被建立1,事件被建立2; 事件’event’建立一次性事件,觸發事件建立監聽器1,2,列印:事件被建立1,事件被建立2; log列印:11111111111111111; emit觸發事件event,觸發解綁事件監聽器,處理事件佇列裡的event事件, 列印:有事件被解綁,事件處理函式被呼叫; log列印:22222222222222222; 由於事件event被解綁,無法被呼叫,後面沒有列印結果。。。
這裡需要注意的就是,nodejs裡的事件佇列的事件是在監聽建立觸發後再執行事件的觸發