JS Pattern #4 - PubSub Pattern

Posted on April 6th, 2018

JavaScript PubSub Pattern, hay Publish / Subscribe Pattern, hay Events Pattern, là một loại JavaScript Design Pattern rất hữu dụng và khá phổ biến, được sử dụng trong rất nhiều thư viện JavaScript. Sau đây là một cách triển khai Pubsub Pattern cực kỳ ngắn gọn và dễ hiểu.

Triển khai PubSub Pattern

let Events = (() => {
  let events = {};
  function on(eventName, fn) {
    events[eventName] = events[eventName] || [];
    events[eventName].push(fn);
  }
  function off(eventName, fn) {
    if (events[eventName]) {
      for (let i = 0; i < events[eventName].length; i++) {
        if (events[eventName][i] === fn) {
          events[eventName].splice(i, 1);
          break;
        }
      };
    }
  }
  function emit(eventName, data) {
    if (events[eventName]) {
      events[eventName].forEach((fn) => fn(data));
    }
  }
  return {
    on : on,
    off: off,
    emit : emit
  };
})();

Trước tiên, bạn thấy rằng cách triển khai pattern này sử dụng Revealing Module Pattern, với 3 function được public là: on, offemit.

Đăng ký sự kiện với hàm on

Hàm on có 2 tham số đầu vào:

  • eventName: tên sự kiện
  • fn: hàm xử lý sự kiện tương ứng.

Ví dụ:

Events.on("addItem", onItemAdded);

Câu lệnh trên hiểu đơn giản là: khi có sự kiện addItem xảy ra thì sẽ gọi hàm onItemAdded.

Giải thích cách triển khai

let events = {};
function on(eventName, fn) {
  events[eventName] = events[eventName] || [];
  events[eventName].push(fn);
}

Bạn có thể thấy rằng, events là một object rỗng. Sau này, mỗi thuộc tính (key) của events sẽ là tên của event được đăng ký. Giá trị của thuộc tính (value) là một mảng của các function.

Ban đầu events[eventName] sẽ là undefined, nên ta sẽ khởi tạoevents[eventName] = [] - là một mảng rỗng. Ngược lại, khi thành phần này đã tồn tại thì chỉ cần push hàm số mới vào mảng.

Đối với ví dụ trên, events["addItem"] = [onItemAdded].

Giả sử, có 2 module khác cũng đăng ký sự kiện addItem:

Events.on("addItem", updateItem1);
Events.on("addItem", updateItem2);

Lúc này, events["addItem"] = [onItemAdded, updateItem1, updateItem2].

Hủy đăng ký sự kiện với hàm off

Hàm off có 2 tham số đầu vào:

  • eventName: tên sự kiện
  • fn: hàm đã đăng ký sự kiện, cần hủy đăng ký

Ví dụ:

Events.off("addItem", onItemAdded);

Câu lệnh trên sẽ bỏ đăng ký sự kiện addItem với hàm onItemAdded. Hay nói cách khác, khi có sự kiện này xảy ra thì sẽ không gọi hàm onItemAdded nữa.

Giải thích cách triển khai

function off(eventName, fn) {
  if (events[eventName]) {
    for (let i = 0; i < events[eventName].length; i++) {
      if (events[eventName][i] === fn) {
        events[eventName].splice(i, 1);
        break;
      }
    };
  }
}

Hàm số này sẽ duyệt mảng ứng với eventName và kiểm tra tất cả các hàm số thành phần trong mảng đó. Nếu gặp hàm số nào trùng với hàm số cần bỏ đăng ký thì sẽ bỏ hàm số đó ra khỏi mảng thông qua hàm splice.

Đối với ví dụ trên, hàm số onItemAdded sẽ bị loại bỏ khỏi mảng events["addItem"]. Do đó, kết quả là events["addItem"] = [updateItem1, updateItem2].

Kích hoạt sự kiện với hàm emit

Hàm emit có 2 tham số:

  • eventName: tên sự kiện
  • data: dữ liệu truyền cho các hàm đã đăng ký sự kiện, trong đó, data có thể là tất cả các kiểu dữ liệu: number, array, string hay object,...

Ví dụ:

Events.emit("addItem", {
  name : "apple",
  quantity: 2
});

Câu lệnh trên kích hoạt sự kiện addItem. Khi đó, tất cả những hàm số nào đã được đăng ký sự kiện với hàm Events.on ở trên, sẽ được gọi, với đối số là object {name: "apple", quantity: 2};

Giải thích cách triển khai

function emit(eventName, data) {
  if (events[eventName]) {
    events[eventName].forEach((fn) => fn(data));
  }
}

Hàm này sẽ duyệt toàn bộ mảng ứng với eventName sử dụng phương thức forEach. Sau đó, mỗi hàm số trong mảng sẽ được gọi và truyền vào đối số là data.

Trong ví dụ trên, events["addItem"] = [updateItem1, updateItem2]. Do đó, khi event addItem được kích hoạt, hàm số updateItem1({ name : "apple", quantity: 2 }) và updateItem2({ name : "apple", quantity: 2 }) sẽ được thực hiện.

Ví dụ sử dụng Pubsub Pattern

Trong đó, module A sẽ thực hiện addItem và kích hoạt sự kiện. Và module B, module C sẽ đăng kí sự kiện addItem và với hàm số thực thi là onAddingItem.

Chú ý:

  • Module B và module C đều đăng ký sự kiện với hàm số có cùng tên là onAddingItem. Nhưng thực tế là chúng khác nhau vì 2 hàm này nằm ở module khác nhau, phạm vi biến sẽ khác nhau.
  • Trong ví dụ này, các module đều đặt trong cùng 1 file, tuy nhiên bạn có thể đặt mỗi module tách riêng ra các file khác nhau.

Kết luận

Trên đây là cách triển khai PubSub Pattern và ví dụ đơn giản mô tả cách áp dụng pattern này. Nếu có phần nào mình giải thích chưa được rõ thì các bạn cứ đặt câu hỏi thoải mái nhé.

Xin chào và hẹn gặp lại bạn ở bài viết tiếp theo, thân ái!


★ Nếu bạn thấy bài viết này hay thì hãy theo dõi mình trên Facebook để nhận được thông báo khi có bài viết mới nhất nhé: