Bài 7: NodeJS - Sự kiện Emitter - NodeJS cơ bản cho người mới bắt đầu

Đăng bởi: Admin | Lượt xem: 1724 | Chuyên mục: NodeJS


Nhiều đối tượng trong Node.js sinh ra các sự kiện, ví dụ net.Server sinh ra một sự kiện mỗi khi có một kết nối ngang hàng đến nó, hay fs.readStream sinh ra sự kiện khi một file được mở. Tất cả các đối tượng này đều là sự thể hiện của lớp events.EventEmitter trong Node.js.

1. Lớp EventEmitter trong Node.js

Lớp EventEmitter nằm trong events Module. Lớp này được truy cập qua cú pháp sau:
// Import events module
var events = require('events');

// Create an eventEmitter object
var eventEmitter = new events.EventEmitter();
Khi một EventEmitter gặp bất kì lỗi nào, nó sẽ sinh ra một Error Event. Khi một Listener mới được thêm, sự kiện 'newListener' sẽ được kích hoạt và một Listener sẽ bị loại bỏ, sự kiện 'removeListener' sẽ được kích hoạt.
Event Emitter cung cấp nhiều thuộc tính như on hay emit. Thuộc tính on được sử dụng để gắn kết một hàm với sự kiện, và emit dược sử dụng để kích hoạt một sự kiện.
Các phương thức của lớp EventEmitter trong Node.js
  1. addListener(event, listener) : Thêm một Listener vào phần cuối của mảng các Listener cho một sự kiện cụ thể
  2. on(event, listener) : Thêm một Listener vào phần cuối của mảng các Listener cho một sự kiện cụ thể
  3. once(event, listener) : Thêm một One-Time Listener cho sự kiện. Listener dạng này sẽ chỉ được gọi khi sự kiện được kích hoạt, sau đó nó sẽ bị xóa
  4. removeListener(event, listener) : Xóa một Listener ra khỏi mảng các Listener cho một sự kiện nào đó.
  5. removeAllListeners([event]) : Xóa tất cả Listener của một sự kiện
  6. setMaxListeners(n) : Theo mặc định, lớp EventEmitters sẽ in một lời cảnh báo nếu bạn thêm nhiều hơn 10 Listener cho một sự kiện cụ thể. Việc này khá hữu ích, bởi vì nó sẽ giúp tìm ra các lỗi gây rò rỉ bộ nhớ. Tất nhiên, không phải tất cả các Emitters đều cần được giới hạn với con số là 10. Hàm này cho phép bạn tăng con số đó. Thiết lập nó về 0 để không giới hạn lượng Listener cần thêm
  7. listeners(event)Trả về một mảng bao gồm các Listener cho một sự kiện cụ thể nào đó
  8. emit(event, [arg1], [arg2], [...])
    Thực thi từng Listener với các tham số đã cho. Trả về true nếu sự kiện có các Listener, và false nếu không có

2. Tìm hiểu EventEmitter:

a. Cơ chế hoạt động cơ bản của EventEmitter :
Cơ chế hoạt động cơ bản của EventEmitter trong Nodejs có thể được thể hiện qua ví dụ sau:
Trước tiên, mình tạo file emitter.js như sau:
function Emitter() {
  this.events = {};
}

Emitter.prototype.on = function (type, listener) {
  this.events[type] = this.events[type] || [];
  this.events[type].push(listener);
};

Emitter.prototype.emit = function (type) {
  if (this.events[type]) {
    this.events[type].forEach(function(listener) {
      listener();
    });
  }
};

module.exports = Emitter;
Module Emitter gồm một literal object events để lưu giữ tất cả các sự kiện; 2 phương thức prototype là on và emit để có thể sử dụng khi kế thừa prototype. Phương thức on sẽ lưu giữ theo từng kiểu sự kiện, mỗi kiểu sự kiện có một mảng các sự kiện khác nhau và các listener của mỗi kiểu sự kiện sẽ được lưu giữ trong mảng riêng của nó. Phương thức emit được dùng để kiểm tra xem trong object lưu giữ sự kiện đã có sự kiện đó chưa, nếu có sẽ thực hiện phát các listener trong sự kiện đó ra.
Để mô tả cho Emitter này mình minh họa qua đoạn mã sau trong app.js:
var Emitter = require("./emitter");
var emitter = new Emitter();

emitter.on("denvang", function tocdo() {
  console.log("Giảm tốc độ");
});

emitter.on("denvang", function dunglai() {
  console.log("Dừng lại trước vạch giới hạn");
});

var tinhieu = [3, 2, 1];//1: đèn đỏ, 2: đèn vàng, 3: đèn xanh
for (var th of tinhieu) {
  if (th == 2) {
    emitter.emit("denvang");
  }
}
Chương trình mô tả việc nếu tín hiệu giao thông là đèn vàng sẽ phát ra hiệu lệnh trong sự kiện denvang. Trong emitter, mình đã thêm vào object sự kiện, kiểu sự kiện là denvang. Trong kiểu sự kiện đèn vàng có 2 listener là tocdo và dunglai. Khi đèn tín hiệu rơi vào đèn vàng sẽ phát ra các listener đã lưu trong sự kiện với kiểu là denvang. Và kết quả chúng ta nhận được khi gặp đèn vàng sẽ là:
Giảm tốc độ
Dừng lại trước vạch giới hạn
b. Kế thừa EventEmitter :
Sử dụng hàm có sẵn trong thư viện Events(Node.js)
Như mình có giới thiệu qua EventEmitter là một class trong thư việc Events (Node.js) với cơ chế hoạt động được mô phỏng ở phần trên. Trong thực tế, mình không nhất thiết phải viết một module tương tự như ở trên để thực thi các sự kiện lắng nghe mà sử dụng trực tiếp các hàm có sẵn trong thư viện Events (Node.js). Để mô phỏng cho điều này, cũng với ví dụ ở phần trước, mình sẽ minh họa cách sử dụng hàm on và emit sẵn có trong thư viện Events:
var Emitter = require("events");
var emitter = new Emitter();

emitter.on("denvang", function tocdo() {
  console.log("Giảm tốc độ");
});

emitter.on("denvang", function dunglai() {
  console.log("Dừng lại trước vạch giới hạn");
});

var tinhieu = [3, 2, 1];//1: đèn đỏ, 2: đèn vàng, 3: đèn xanh
for (var th of tinhieu) {
  if (th == 2) {
    emitter.emit("denvang");
  }
}
Thay vì require đến module emitter.js do mình tự tạo, mình sử require đến thư viện events, và thông qua kế thừa prototype các hàm sẵn có trong thư viện này. Cuối cùng, mình vẫn nhận được kết quả tương tự cho ví dụ minh họa trên.
Kể thừa và tái sử dụng EventEmitter :
Để kế thừa và tái sử dụng EventEmitter, ở đây, mình giới thiệu phương pháp sử dụng hàm inherits trong thư viện util (Node.js).
Để minh họa, mình tạo apptinhieu.js như sau:
var EventEmitter = require("events");
var util = require("util");

function DenTinHieu(typeOfLight) {
  this.type = typeOfLight;
}

util.inherits(DenTinHieu, EventEmitter);

DenTinHieu.prototype.hieuLenh = function () {
  switch(this.type) {
    case "dendo":
      this.emit("dunglai");
      break;
    case "denvang":
      this.emit("tocdo");
      this.emit("dunglai");
      break;
    case "denxanh":
      this.emit("duocphepdi");
      break;
    default:
      khongCoHieuLenh();
  }

};

var dendo = new DenTinHieu("dendo");

dendo.on("dunglai", dungLai);

var denvang = new DenTinHieu("denvang");

denvang.on("tocdo", tocDo);

denvang.on("dunglai", dungLai);

var denxanh = new DenTinHieu("denxanh");

denxanh.on("duocphepdi", duocPhepDi);

var tinhieu = {1: "dendo", 2: "denvang", 3: "denxanh"};
var tinhieuden = [1, 3, 2, 1];

for(var th of tinhieuden) {
  console.log("Tín hiệu đèn giao thông: ", tinhieu[th]);
  switch(tinhieu[th]) {
    case "dendo":
      dendo.hieuLenh();
      break;
    case "denvang":
      denvang.hieuLenh();
      break;
    case "denxanh":
      denxanh.hieuLenh();
      break;
    default:
      khongCoHieuLenh();
   }
  }

function dungLai() {
  console.log("Dừng lại trước vạch giới hạn");
}

function tocDo() {
  console.log("Giảm tốc độ");
}

function duocPhepDi() {
  console.log("Được phép đi");
}

function khongCoHieuLenh() {
  console.log("Không có hiệu lệnh cho loại đèn tín hiệu này.");
}
Ở đây mình có một đối tượng là DenTinHieu. Đối tượng này sẽ kế thừa các phương thức của EventEmitter trong thư viện events (Node.js). Mình mở rộng đối tượng này thông qua prototype bằng hàm hieuLenh để thực thi hiệu lệnh của loại đèn tín hiệu.
Từ đối tượng DenTinHieu này mình tạo ra 3 đối tượng dendo, denvang, denxanh là thể hiện của DenTinHieu. Vì đối tượng DenTinHieu đã được thừa kế từ EventEmitter nên các thể hiện của nó có thể sử dụng các phương thức của EventEmitter mà không cần phải kế thừa lại. Chính vì vậy các phương thức này đã được tái sử dụng. Bằng cách này, mình thiết lập các sự kiện thông qua phương thức on cho các đối tượng DenTinHieu và phát các sự kiện này ra thông qua phương thức emit.
Thực thi apptinhieu.js trên terminal nodejs apptinhieu.js, mình được kết quả như sau:
Tin hieu đèn giao thông: dendo
Dừng lại trước vạch giới hạn
Tin hieu đèn giao thông: denxanh
Được phép đi
Tin hieu đèn giao thông: denvang
Giảm tốc độ
Dừng lại trước vạch giới hạn
Tin hieu đèn giao thông: dendo
Dừng lại trước vạch giới hạn
Một cách khác, mình sẽ không tạo ra 3 thể hiện của DenTinHieu như trên nữa mà sử dụng một thể hiện chung cho cả 3 loại trên. Lúc này, mình xem 3 loại đèn tín hiện là một data đầu vào cho phương thức on và emit.
DenTinHieu.prototype.hieuLenh = function (loaiDen) {
  console.log(`Tin hieu ${this.type}: ${loaiDen}`);
  this.emit("canhbao", loaiDen);
};

var denGiaoThong = new DenTinHieu("đèn giao thông");

denGiaoThong.on("canhbao", function(loaiDen) {
  switch(loaiDen) {
    case "dendo":
      dungLai();
      break;
    case "denvang":
      tocDo();
      dungLai();
      break;
    case "denxanh":
      duocPhepDi();
      break;
    default:
      khongCoHieuLenh();
  }
});
Ở đây, thể hiện denGiaoThong được tạo ra từ DenTinHieu sẽ đại diện chung cho các loại đèn tín hiệu. Khi thiết lập sự kiện canhbao cho denGiaoThong mình truyền data là loaiden vào phương thức thực thi để xử lý khi emit sự kiện canhbao trên denGiaoThong.
Hoặc mình có thể thực hiện thiết lập sự kiện trên EventEmitter, sau đó, đối tượng DenTinHieu sẽ kế thừa từ EventEmitter. Lúc này thể hiện denGiaoThong cũng đã có sự kiện canhbao được kế thừa, và chỉ cần phát sự kiện ra thông qua phương thức emit.
EventEmitter.on("canhbao", function(loaiDen) {
  switch(loaiDen) {
    case "dendo":
      dungLai();
      break;
    case "denvang":
      tocDo();
      dungLai();
      break;
    case "denxanh":
      duocPhepDi();
      break;
    default:
      khongCoHieuLenh();
  }
});

util.inherits(DenTinHieu, EventEmitter);

DenTinHieu.prototype.hieuLenh = function (loaiDen) {
  console.log(`Tin hieu ${this.type}: ${loaiDen}`);
  this.emit("canhbao", loaiDen);
};

3. Các sự kiện của lớp EventEmitter trong Node.js

1. newListener
  • event - Dạng chuỗi, biểu diễn tên sự kiện
  • listener - Tên hàm xử lý sự kiện
Sự kiện này được sinh bất cứ khi nào bạn thêm một Listener. Khi sự kiện này được kích hoạt, Listener có thể sẽ chưa được thêm vào mảng Listener của sự kiện
2. removeListener
  • event - Dạng chuỗi, biểu diễn tên sự kiện
  • listener - Tên hàm xử lý sự kiện
Sự kiện này xảy ra bất cứ khi nào có ai đó xóa một Listener. Khi một sự kiện được kích hoạt, Listener này chưa được xóa khỏi mảng Listener của sự kiện

4. Ví dụ minh họa lớp EventEmitter trong Node.js

Tạo một file js với tên là main.js với nội dụng Node.js như dưới đây. Trong main.js, đầu tiên bạn khai báo events Module bởi sử dụng phương thức require(). Tiếp đó, bạn sử dụng phương thức addListener() để thêm một Listener cho một sự kiện nào đó, và sử dụng các thuộc tính on và emit để thực hiện các tính năng đã trình bày ở trên.
var events = require('events');
var eventEmitter = new events.EventEmitter();

// listener #1
var listner1 = function listner1() {
   console.log('listner1 executed.');
}

// listener #2
var listner2 = function listner2() {
   console.log('listner2 executed.');
}

// Bind the connection event with the listner1 function
eventEmitter.addListener('connection', listner1);

// Bind the connection event with the listner2 function
eventEmitter.on('connection', listner2);

var eventListeners = require('events').EventEmitter.listenerCount
   (eventEmitter,'connection');
console.log(eventListeners + " Listner(s) listening to connection event");

// Fire the connection event 
eventEmitter.emit('connection');

// Remove the binding of listner1 function
eventEmitter.removeListener('connection', listner1);
console.log("Listner1 will not listen now.");

// Fire the connection event 
eventEmitter.emit('connection');

eventListeners = require('events').EventEmitter.listenerCount(eventEmitter,'connection');
console.log(eventListeners + " Listner(s) listening to connection event");

console.log("Program Ended.");
Chạy main.js để xem kết quả:
$ node main.js
Kết quả hiển thị như sau :
2 Listner(s) listening to connection event
listner1 executed.
listner2 executed.
Listner1 will not listen now.
listner2 executed.
1 Listner(s) listening to connection event
Program Ended.
Bài tiếp theo: NodeJs - Buffer >>
vncoder logo

Theo dõi VnCoder trên Facebook, để cập nhật những bài viết, tin tức và khoá học mới nhất!