رویدادها (Events) در Node.js — راهنمای استفاده صحیح

آخرین به‌روزرسانی: ۲۰ آبان ۱۳۹۷
زمان مطالعه: ۴ دقیقه

تا پیش از مطرح شدن برنامه‌نویسی رویدادمحور (event-driven)، روش استاندارد برای ارتباط بین بخش‌های مختلف یک اپلیکیشن کاملاً سرراست بود. کامپوننتی که می‌خواست پیامی را به کامپوننت دیگر ارسال کند، یکی از متدهای آن را فراخوانی می‌کرد. اما برنامه‌نویسی رویدادمحور برای نوشتن «واکنش»، به جای «فراخوانی» مطرح شده است. در این نوشته به توضیح روش صحیح استفاده از رویدادهای Node.js می پردازیم.

مزیت‌های رویداد محور بودن (Eventing)

این رویکرد موجب می‌شود که کامپوننت‌های ما تا حد امکان مستقل از هم باشند. در واقع وقتی مشغول نوشتن یک اپلیکیشن هستیم، رویدادهای ممکن را در طی مسیر شناسایی می‌کنیم، آن‌ها را در زمان صحیح اجرا می‌نماییم و یک یا چند شنونده رویداد (event listener) به آن‌ها الصاق می‌کنیم. به این ترتیب بسط کارکردها بدون نیاز به دستکاری شنونده‌ها یا بخش‌های دیگری از اپلیکیشن که رویداد از آنجا آغاز می‌شود، آسان‌تر صورت می‌پذیرد. در واقع ما اکنون در مورد الگوی ناظر (Observer) صحبت می‌کنیم.

طراحی یک معماری رویداد محور

شناسایی رویدادها بسیار حائز اهمیت است، زیرا ما نمی‌خواهیم در انتها رویدادهای موجود را از سیستم حذف یا جایگزین کنیم، چون باعث می‌شود مجبور شویم تعدادی از شنونده‌هایی که به رویدادها متصل هستند را حذف یا اصلاح کنیم. یک اصل در این مورد چنین است که «تنها زمانی یک رویداد باید آغاز شود که اجرای یک منطق تجاری پایان می‌یابد».

بنابراین با فرض این که می‌خواهید بک دسته از ایمیل‌های مختلف را پس از ثبت نام کاربر ارسال کنید. با توجه به این که فرایند ثبت نام خود می‌تواند شامل مراحل پیچیده و کوئری‌های مختلفی باشد؛ اما از دیدگاه تجاری آن را یک مرحله در نظر می‌گیریم و هر یک از ایمیل‌های که باید ارسال شوند نیز یک مرحله هستند. بنابراین فعالسازی یک رویداد درست پس از پایان ثبت نام و الصاق چند شنونده به آن که هر یک مسئول ارسال نوع خاصی از ایمیل هستند منطقی به نظر می‌رسد.

معماری ناهمگام و رویدادمحور Node انواع خاصی از شیءها به نام emmiter دارد که وظیفه‌شان ارسال رویدادهای دارای نام است که موجب می‌شوند کارکردهایی به نام listener ها فراخوانی شوند. همه شیءهایی که رویدادها را می‌فرستند وهله‌هایی از کلاس EventEmitter هستند. با استفاده از این کلاس می‌توانیم رویدادهای خاص خود را بسازیم:

مثال

فرض کنید از ماژول رویداد توکاری برای کسب دسترسی به EventEmitter استفاده کنیم:

const EventEmitter = require('events');

const myEmitter = new EventEmitter();

module.exports = myEmitter;

این بخشی از اپلیکیشن است که سرور ما یک درخواست HTTP دریافت می‌کند، یک کاربر جدید ذخیره می‌کند و رویداد مربوطه را ارسال می‌کند:

const myEmitter = require('./my_emitter');

// Perform the registration steps

// Pass the new user object as the message passed through by this event.
myEmitter.emit('user-registered', user);

و یک ماژول جداگانه وجود دارد که برای الصاق یک شنونده استفاده می‌کنیم:

const myEmitter = require('./my_emitter');

myEmitter.on('user-registered', (user) => {
   // Send an email or whatever.
});

جدا کردن سیاست از پیاده‌سازی همواره یک رویه مناسب محسوب می‌شود. در این مورد، سیاست به معنی این است که شنونده‌ها برای کدام رویدادها ثبت شده‌اند و پیاده‌سازی به معنی خود شنونده‌ها است.

const myEmitter = require('./my_emitter');
const sendEmailOnRegistration = require('./send_email_on_registration');
const someOtherListener = require('./some_other_listener');


myEmitter.on('user-registered', sendEmailOnRegistration);

myEmitter.on('user-registered', someOtherListener);
module.exports = (user) => {
   // Send a welcome email or whatever.
}

این جداسازی به شنونده‌ها امکان استفاده مجدد نیز می‌دهد، یعنی می‌توان آن را به رویدادهای دیگر که همان پیام را ارسال می‌کنند الصاق کرد. همچنین لازم به ذکر است که وقتی چند شنونده به یک رویداد منفرد متصل می‌شوند، به طور همگام و بر حسب ترتیبی که الصاق یافته‌اند، اجرا می‌شوند. از این رو someOtherListener پس از این که اجرای sendEmailOnRegistration پایان یافت، اجرا می‌شود.

با این وجود، اگر بخواهیم شنونده‌ها به صورت همزمان اجرا شوند، باید پیاده‌سازی آن‌ها را به صورت زیر در یک پوشش setImmediate قرار دهیم:

module.exports = (user) => {
   setImmediate(() => {
      // Send a welcome email or whatever.
   });
}

مرتب نگه داشتن شنونده‌ها

زمانی که مشغول نوشتن «شنونده‌ها» (Listeners) هستیم باید به اصل «مسئولیت منفرد» (Single Responsibility) پایبند باشیم، یعنی شنونده‌ها باید تنها یک کار انجام دهند تا آن را به خوبی انجام دهند. برای نمونه باید از نوشتن شروط زیاد درون یک شنونده که تعیین می‌کنند بر اساس داده‌های (پیام) فرستاده شده از سوی رویداد، چه چیزی باید ارسال شود، اجتناب کرد. در این حالت استفاده از چندین شنونده مختلف بسیار بهتر است:

const myEmitter = require('./my_emitter');

// Perform the registration steps

// The application should react differently if the new user has been activated instantly.
if (user.activated) {
   myEmitter.emit('user-registered:activated', user);

} else {
   myEmitter.emit('user-registered', user);
}
const myEmitter = require('./my_emitter');
const sendEmailOnRegistration = require('./send_email_on_registration');
const someOtherListener = require('./some_other_listener');
const doSomethingEntirelyDifferent = require('./do_something_entirely_different');
 

myEmitter.on('user-registered', sendEmailOnRegistration);
myEmitter.on('user-registered', someOtherListener);

myEmitter.on('user-registered:activated', doSomethingEntirelyDifferent);

جداسازی شنونده‌ها به صورت صریح در موارد نیاز

در مثال قبلی شنونده‌ها به طور کامل تابع‌های مستقلی بودند. اما در مواردی که شنونده با یک شیء مرتبط باشد، (یعنی متد شیء باشد) باید آن را به صورت دستی از رویدادهایی که در آن‌ها ثبت شده است جدا کنیم. در غیر این صورت هرگز از سوی garbage-collector جمع‌آوری نمی‌شود، چون بخشی از شیء (شنونده) است و همچنان از سوی شیء بیرونی (emitter) مورد ارجاع قرار می‌گیرد. از این رو احتمال نشت حافظه (memory-leak) وجود دارد.

برای نمونه اگر مشغول ساخت یک اپلیکیشن چت هستید و می‌خواهید مسئولیت نمایش اعلان‌ها هنگام رسیدن پیام‌های جدید در اتاق چت را بر عهده بگیرید. باید آن را درون همان شیء کاربری که به اتاق چت متصل شده است به صورت زیر پیاده‌سازی کنید:

class ChatUser {

   displayNewMessageNotification(newMessage) {
      // Push an alert message or something.
   }

   // `chatroom` is an instance of EventEmitter.
   connectToChatroom(chatroom) {
      chatroom.on('message-received', this.displayNewMessageNotification);
}

   disconnectFromChatroom(chatroom) {
      chatroom.removeListener('message-received', this.displayNewMessageNotification);
   }

}

وقتی کاربر برگه مرورگر خود را می‌بندد و یا برای مدتی اتصال اینترنتی‌اش قطع می‌شود، به طور طبیعی می‌خواهیم که یک callback در سمت سرور ایجاد کنیم که به کاربران دیگر اعلان می‌کند که یکی از آن‌ها آفلاین شده است. در این حالت، فراخوانی displayNewMessageNotification برای کاربر آفلاین هیچ معنا ندارد؛ اما این شیء همچنان به دریافت پیام‌های جدید ادامه می‌دهد؛ مگر این که به طور صریح آن را حذف کنیم. اگر چنین نکنیم، علاوه بر فراخوانی‌های غیر ضروری، شیء کاربر نیز به صورت نامحدود در حافظه می‌ماند. بنابراین اطمینان حاصل کنید که در callback سمت سرور که هر زمان کاربر آفلاین می‌شود اجرا می‌شود، disconnectFromChatroom را فراخوانی می‌کنید.

سخن پایانی

دقت کنید که اتصال‌های سستی که در معماری رویدادمحور وجود دارند، ممکن است در صورتی که توجه کافی نداشته باشیم منجر به حالت‌های پیچیده‌ای بشوند. در این حالت، ردگیری وابستگی‌ها در سیستم به کاری دشوار تبدیل می‌شود و نمی‌توان تشخیص داد که کدام شنونده‌ها روی کدام رویدادها کار خود را به پایان برده‌اند. اپلیکیشن‌ها در صورتی که شروع به ارسال رویدادها درون شنونده‌ها بکنند و به طور مثال زنجیره‌هایی از رویدادهای غیرمنتظره را راه‌اندازی بکنند، بیشتر در معرض این آسیب‌ها قرار خواهند داشت.

اگر این مطلب برایتان مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۵ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
usamaashraf

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *