اولین بار که با رویدادها در جاوااسکریپت جدی کار کردم، یک باگ عجیب داشتم: دکمه‌ای که با کلیک، یک مودال را باز می‌کرد، بعد از باز شدن، بلافاصله بسته می‌شد. ساعتی وقت گذاشتم تا بفهمم چه اتفاقی می‌افتد. مشکل، در یک رویداد پنهان بود: کلیک روی دکمه، به والد آن هم می‌رسید و همان والد، مودال را می‌بست. آن روز برایم روشن شد که رویدادها در جاوااسکریپت فقط addEventListener نیستند؛ یک مدل ارتباطی پیچیده‌اند که اگر مسیر جریانشان را نفهمید، هر کلیک کاربر می‌تواند منبع باگ‌های نامرئی باشد. در این مقاله، همان مسیری را می‌روم که امروز با تازه‌کارها طی می‌کنم: از مدل رویدادی مرورگر و انواع رویدادها تا Event Bubbling، Delegation، Custom Events و بهینه‌سازی در پروژه‌های واقعی.

رویداد دقیقاً چیست؟

اگر تازه با جاوااسکریپت آشنا می‌شوید، اول آموزش جاوااسکریپت از صفر را بخوانید و بعد مفاهیم پایه جاوااسکریپت را مرور کنید. حالا سؤال اصلی: رویداد چیست؟

رویداد، یک سیگنال است که از سمت مرورگر یا کاربر می‌آید و به کد شما می‌گوید «یک اتفاق افتاد». این اتفاق می‌تواند هر چیزی باشد: کلیک کاربر روی دکمه، تایپ حرف در یک فیلد، اسکرول صفحه، بارگذاری یک تصویر، موفقیت یا شکست یک درخواست شبکه، حتی فشار کلید Escape. کد شما می‌تواند به این سیگنال‌ها «گوش بدهد» و واکنش نشان دهد.

سه نکته‌ی مهم در مدل ذهنی رویدادها که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • رویداد، جدای از DOM است: هر گره DOM می‌تواند رویداد دریافت کند، ولی خودِ رویداد، یک آبجکت جداگانه است که اطلاعاتی درباره‌ی اتفاق حمل می‌کند.
  • رویدادها آسنکرون هستند: وقتی کاربر کلیک می‌کند، کد شما مستقیم اجرا نمی‌شود؛ بلکه در صف رویدادها قرار می‌گیرد و بعد از تمام شدن کد جاری، اجرا می‌شود. این همان مفهوم Event Loop است که در مفاهیم پایه توضیح داده‌ام.
  • رویدادها در سلسله‌مراتبی جریان دارند: وقتی روی یک دکمه کلیک می‌کنید، رویداد از بالا به پایین و بعد از پایین به بالا جریان می‌یابد. این رفتار که Event Bubbling و Capture نام دارد، منبع بسیاری از باگ‌های پنهان و در عین حال، پایه‌ی الگوهای قدرتمندی مثل Delegation است.
رویداد، یک پیام از دنیای کاربر به دنیای کد شماست؛ اگر مسیر این پیام را نشناسید، نمی‌دانید چرا کدتان در جای اشتباه واکنش نشان می‌دهد.

سه روش اتصال رویداد

در جاوااسکریپت، سه روش برای اتصال رویداد وجود دارد که هرکدام ویژگی خودشان را دارند:

۱) HTML attribute (روش قدیمی، توصیه نمی‌شود)

<button onclick="handleClick()">کلیک کن</button>

این روش، منطق جاوااسکریپت را با HTML مخلوط می‌کند و در پروژه‌های واقعی، نگهداری را به کابوس تبدیل می‌کند. تنها مزیتش، سادگی برای مثال‌های کوچک است.

۲) خاصیت on-event (روش میانه)

const button = document.querySelector("button");
button.onclick = function () {
    console.log("Clicked!");
};

مشکل این روش: فقط یک listener می‌توانید برای هر رویداد داشته باشید. اگر دوباره onclick تعیین کنید، listener قبلی بازنویسی می‌شود. در پروژه‌های واقعی، این ویژگی منبع باگ‌های پنهان است — مخصوصاً وقتی چند ماژول مختلف بخواهند به یک رویداد گوش بدهند.

۳) addEventListener (روش استاندارد)

const button = document.querySelector("button");

button.addEventListener("click", () => {
    console.log("First listener");
});

button.addEventListener("click", () => {
    console.log("Second listener");
});

مزیت کلیدی: چند listener برای یک رویداد می‌توانید داشته باشید، و هر کدام مستقل عمل می‌کند. در پروژه‌های واقعی، همیشه از این روش استفاده می‌کنم — تنها استثنا، زمانی است که می‌خواهم در تست کوچکی، یک listener موقت اضافه کنم.

حذف listener

function handleClick() {
    console.log("Clicked!");
}

button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);

نکته‌ی حیاتی: برای حذف، باید همان reference تابع را بدهید. arrow functionهای بی‌نام قابل حذف نیستند چون هر بار reference متفاوتی دارند. این اشتباه در پروژه‌های واقعی، به نشت حافظه‌ی پنهان منجر می‌شود.

// اشتباه — قابل حذف نیست
button.addEventListener("click", () => console.log("clicked"));
button.removeEventListener("click", () => console.log("clicked")); // بی‌اثر

// درست — قابل حذف است
const handler = () => console.log("clicked");
button.addEventListener("click", handler);
button.removeEventListener("click", handler);

رویدادهای پرکاربرد در پروژه‌های واقعی

صدها نوع رویداد در مرورگر وجود دارد، ولی در پروژه‌های واقعی، یک هسته‌ی کوچک، ۹۰٪ کارها را انجام می‌دهد:

رویدادروی چه عنصریکاربرد
clickهر عنصریدکمه‌ها، لینک‌ها، کارت‌های تعاملی
inputفرمجستجوی زنده، اعتبارسنجی بلادرنگ
changeفرمانتخاب از dropdown، checkbox
submitفرمارسال فرم، اعتبارسنجی نهایی
keydown / keyupهر عنصر قابل فوکوسکلیدهای میانبر، جستجو
scrollwindow، هر اسکرول‌پذیرانیمیشن‌های اسکرول، sticky هدر
resizewindowریسپانسیو پویا
DOMContentLoadeddocumentاجرای کد بعد از آماده‌شدن DOM
loadwindow، تصویراطمینان از بارگذاری کامل منابع
focus / blurفرمرنگ‌بندی فیلد فعال

در پروژه‌های واقعی، بیشتر وقت من صرف کار با click، input، submit و scroll می‌شود. بقیه‌ی رویدادها بر اساس نیاز پروژه‌های خاص، اضافه می‌شوند — مثلاً touchstart در پروژه‌های موبایل‌محور یا dragstart در پروژه‌های تعاملی.

شیء Event: اطلاعات پشت هر کلیک

هر listener، یک شیء Event دریافت می‌کند که پر از اطلاعات مفید است:

button.addEventListener("click", (event) => {
    console.log(event.target);         // عنصری که کلیک رویش رخ داد
    console.log(event.currentTarget);  // عنصری که listener رویش نصب شده
    console.log(event.type);           // "click"
    console.log(event.timeStamp);      // زمان رویداد
    console.log(event.clientX);        // مختصات افقی موس
    console.log(event.clientY);        // مختصات عمودی موس
});

تفاوت target و currentTarget، یکی از مهم‌ترین مفاهیمی است که در پروژه‌های واقعی زیاد به آن برخورده‌ام:

  • event.target: عنصری که رویداد واقعاً رویش رخ داد — می‌تواند یک فرزند عمیق باشد.
  • event.currentTarget: عنصری که listener رویش نصب شده — در طول اجرای listener ثابت است.

این تفاوت، پایه‌ی Event Delegation است که در ادامه توضیح می‌دهم. اگر با کار با DOM آشنا هستید، تفاوت این دو ویژگی همان تفاوت parent و child در درخت DOM است.

Event Bubbling و Capture

وقتی روی یک دکمه کلیک می‌کنید، رویداد از دو مسیر عبور می‌کند:

  1. Capture (پایین‌رو): از ریشه (window) تا عنصری که کلیک رویش رخ داده.
  2. Target: عنصر هدف، جایی که رویداد رخ داده.
  3. Bubbling (بالارو): از عنصر هدف، به سمت والدها و تا ریشه.
// HTML:
// <div class="parent">
//   <button class="child">کلیک کن</button>
// </div>

const parent = document.querySelector(".parent");
const child = document.querySelector(".child");

// حالت پیش‌فرض: bubbling
parent.addEventListener("click", () => console.log("Parent"));
child.addEventListener("click", () => console.log("Child"));

// کلیک روی button → خروجی: Child, Parent

// با capture
parent.addEventListener("click", () => console.log("Parent (capture)"), true);
// کلیک روی button → خروجی: Parent (capture), Child, Parent

نکته‌ی ظریفی که در پروژه‌های واقعی به آن رسیده‌ام: Bubbling باعث می‌شود کلیک روی یک عنصر فرزند، به‌عنوان کلیک روی والد هم تلقی شود. این ویژگی گاهی مفید است (پایه‌ی Delegation) و گاهی مشکل‌ساز (بستن ناخواسته‌ی مودال هنگام کلیک روی محتوای آن). در بخش stopPropagation به راه‌حل این مشکل می‌رسم.

در مدل رویدادی جاوااسکریپت، هر کلیک، یک سفر از ریشه تا برگ و بازگشت است؛ اگر مسیر را نشناسید، گاهی کدتان در جای اشتباه واکنش نشان می‌دهد.

Event Delegation: الگوی طلایی لیست‌ها

Event Delegation یکی از قدرتمندترین الگوهایی است که در پروژه‌های واقعی بسیار به‌کار می‌برم. ایده: به‌جای اضافه‌کردن listener به هر عنصر جداگانه، یک listener روی والد مشترک اضافه می‌کنیم:

// بدون Delegation — به هر li یک listener
const items = document.querySelectorAll("li");
items.forEach((item) => {
    item.addEventListener("click", handleClick);
});

// با Delegation — یک listener روی والد
const list = document.querySelector("ul");
list.addEventListener("click", (event) => {
    const item = event.target.closest("li");
    if (!item) return;

    console.log("Clicked on:", item.textContent);
});

مزیت‌های این الگو در پروژه‌های واقعی:

  • کارایی: یک listener به‌جای صدها. در لیست‌های حجیم، تفاوت بین روانی و لَگ‌دار.
  • پویایی: عناصری که بعداً به DOM اضافه می‌شوند، خودکار پوشش داده می‌شوند — نیازی به اتصال مجدد listener نیست.
  • نگهداری آسان‌تر: یک نقطه‌ی مدیریت به‌جای صدها نقطه.

نکته‌ی مهم: متد closest() ابزار اصلی Delegation است — چون از عنصر هدف، به سمت والدین حرکت می‌کند تا عنصری با سلکتور مشخص پیدا کند. اصول کامل کار با این متد در کار با DOM در جاوااسکریپت آمده است.

preventDefault و stopPropagation

دو متد پرکاربرد که در پروژه‌های واقعی زیاد به آن‌ها برمی‌خورم:

preventDefault: لغو رفتار پیش‌فرض

const form = document.querySelector("form");

form.addEventListener("submit", (event) => {
    event.preventDefault();  // جلوگیری از ارسال و رفرش صفحه

    // اعتبارسنجی و ارسال با fetch
    validateAndSubmit();
});

const link = document.querySelector("a.internal");
link.addEventListener("click", (event) => {
    event.preventDefault();  // جلوگیری از ناوبری پیش‌فرض
    router.navigate(link.href);
});

در پروژه‌های واقعی، preventDefault در فرم‌ها و لینک‌های SPA، پرکاربردترین حالت استفاده است.

stopPropagation: متوقف کردن Bubbling

const modal = document.querySelector(".modal");

modal.addEventListener("click", (event) => {
    event.stopPropagation();  // کلیک روی مودال، به والد نرسد
    console.log("Clicked inside modal");
});

document.addEventListener("click", () => {
    closeModal();  // فقط اگر کلیک بیرون مودال باشد
});

این الگو، یکی از رایج‌ترین سناریوها برای stopPropagation است: بستن مودال با کلیک بیرون از آن، بدون این‌که کلیک روی محتوای مودال باعث بسته شدن شود. الگوی مشابه در منوهای کشویی هم به‌کار می‌رود.

نکته‌ی مهم از تجربه: stopPropagation را با احتیاط استفاده کنید. اگر بیش‌ازحد از آن استفاده شود، Event Delegation روی والدها بی‌اثر می‌شود و دیباگ را سخت می‌کند. تنها وقتی استفاده کنید که واقعاً لازم است.

رویدادهای کیبورد و ماوس

رویدادهای کیبورد

document.addEventListener("keydown", (event) => {
    // کلیدهای میانبر
    if (event.key === "Escape") {
        closeModal();
    }

    if (event.ctrlKey && event.key === "s") {
        event.preventDefault();
        saveDocument();
    }
});

سه نکته‌ی مهم در رویدادهای کیبورد که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • key در برابر keyCode: event.key استاندارد مدرن است ("Escape"، "Enter"). event.keyCode قدیمی است و به‌دلیل تفاوت بین مرورگرها و صفحه‌کلیدها، توصیه نمی‌شود.
  • کلیدهای ترکیبی: event.ctrlKey، event.shiftKey، event.altKey و event.metaKey (Command در مک) برای تشخیص ترکیب‌ها به‌کار می‌روند.
  • دسترس‌پذیری: کلید Escape برای بستن مودال، Enter برای تأیید، و Tab برای پیمایش — این‌ها استانداردهایی هستند که کاربران انتظار دارند. اصول کامل در استانداردهای دسترس‌پذیری وب آمده است.

رویدادهای ماوس

element.addEventListener("mouseenter", () => {
    element.classList.add("hover");
});

element.addEventListener("mouseleave", () => {
    element.classList.remove("hover");
});

element.addEventListener("mousemove", (event) => {
    console.log(event.clientX, event.clientY);
});

تفاوت mouseenter/mouseleave با mouseover/mouseout مهم است: اولی‌ها Bubbling ندارند و فقط روی خود عنصر فعال می‌شوند، ولی دومی‌ها Bubble می‌کنند و روی فرزندان هم فعال می‌شوند. در پروژه‌های واقعی، برای منوهای ساده، mouseenter انتخاب درست‌تری است — چون از فعال‌شدن ناخواسته روی فرزندان جلوگیری می‌کند.

رویدادهای لمسی و موبایل

در پروژه‌های واقعی، تقریباً هر پروژه‌ای نیاز به پشتیبانی از لمس دارد. سه رویداد اصلی لمسی:

element.addEventListener("touchstart", (event) => {
    console.log("Touch started", event.touches[0].clientX);
});

element.addEventListener("touchmove", (event) => {
    console.log("Moving...");
});

element.addEventListener("touchend", (event) => {
    console.log("Touch ended");
});

سه نکته‌ی مهم در رویدادهای لمسی که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • چند لمس همزمان: event.touches آرایه‌ای از همه‌ی لمس‌های فعال است. برای تشخیص pinch-to-zoom، از event.touches.length استفاده کنید.
  • تداخل با click: در موبایل، بعد از touchend معمولاً click هم اجرا می‌شود که باعث اجرای دوباره‌ی کد می‌شود. راه‌حل: با event.preventDefault() جلوی آن را بگیرید یا یکی از این دو رویداد را انتخاب کنید.
  • passive listener: در رویدادهای لمسی و touchmove، مرورگر پیش‌فرض از passive استفاده می‌کند — یعنی در صورت preventDefault، ممکن است بی‌اثر باشد. برای جزئیات، بخش Once و Passive را ببینید.

در پروژه‌های واقعی، برای تجربه‌ی موبایل بهتر، از Pointer Events استفاده می‌کنم که هم ماوس و هم لمس را یکسان مدیریت می‌کند:

element.addEventListener("pointerdown", (event) => {
    console.log("Pointer type:", event.pointerType); // "mouse" یا "touch"
});

رویدادهای فرم

فرم‌ها، یکی از پرکاربردترین بخش‌ها در تعامل کاربر هستند. سه رویداد اصلی که در پروژه‌های واقعی زیاد به‌کار می‌برم:

input: هر تغییر در فیلد

const searchInput = document.querySelector("#search");

searchInput.addEventListener("input", (event) => {
    const value = event.target.value;
    console.log("Searching for:", value);
});

رویداد input با هر تغییر فیلد (شامل paste و delete) فعال می‌شود — مناسب جستجوی زنده و اعتبارسنجی بلادرنگ.

change: تغییر بعد از خروج از فیلد

const select = document.querySelector("select");

select.addEventListener("change", (event) => {
    console.log("Selected:", event.target.value);
});

رویداد change روی select، checkbox و radio کاربرد اصلی را دارد. روی text input، فقط وقتی فعال می‌شود که کاربر از فیلد خارج شود و مقدار تغییر کرده باشد.

submit: ارسال فرم

const form = document.querySelector("form");

form.addEventListener("submit", async (event) => {
    event.preventDefault();

    const formData = new FormData(form);
    const data = Object.fromEntries(formData);

    try {
        const response = await fetch("/api/submit", {
            method: "POST",
            body: JSON.stringify(data),
            headers: { "Content-Type": "application/json" },
        });

        if (!response.ok) throw new Error("Server error");
        showSuccess();
    } catch (error) {
        showError(error.message);
    }
});

نکته‌ی امنیتی مهم: اعتبارسنجی سمت کلاینت، فقط برای تجربه‌ی کاربری است. هرگز به آن به‌عنوان لایه‌ی امنیتی تکیه نکنید — همیشه سمت سرور هم اعتبارسنجی کنید. اصول کامل این لایه‌بندی را در امنیت در PHP و امنیت API توضیح داده‌ام. اگر فرم را با fetch ارسال می‌کنید، اصول آن در fetch API در جاوااسکریپت آمده است.

Custom Events: ارتباط بین کامپوننت‌ها

گاهی نیاز دارید رویداد خودتان را بسازید — مثلاً برای ارتباط بین دو بخش از اپلیکیشن که مستقل طراحی شده‌اند:

// ارسال رویداد سفارشی
const event = new CustomEvent("userLoggedIn", {
    detail: { userId: 42, username: "Ali" },
    bubbles: true,
});

document.dispatchEvent(event);

// گوش دادن به رویداد
document.addEventListener("userLoggedIn", (event) => {
    console.log("User logged in:", event.detail.userId);
});

Custom Events در پروژه‌های واقعی، در سه سناریو به‌کار می‌آیند:

  • ارتباط بین کامپوننت‌های مستقل: وقتی دو بخش از صفحه باید بدون وابستگی مستقیم با هم حرف بزنند.
  • گزارش رویدادهای دامنه: مثلاً orderPlaced، itemAddedToCart — که ابزارهای تحلیلی می‌توانند به آن‌ها گوش بدهند.
  • APIهای کامپوننتی: وقتی می‌خواهید یک کامپوننت را به‌عنوان کتابخانه منتشر کنید، Custom Events یکی از بهترین روش‌های ارتباطی است.

ویژگی detail، محل قرار دادن داده‌ی همراه رویداد است. استفاده از bubbles: true باعث می‌شود رویداد، مثل رویدادهای معمول، در درخت DOM جریان پیدا کند — که ترکیب آن با Event Delegation را ممکن می‌کند.

Once و Passive: تنظیمات مدرن listener

addEventListener یک پارامتر سوم دارد که می‌تواند تنظیمات مهمی داشته باشد:

once: اجرای یک‌باره

button.addEventListener("click", handleClick, { once: true });

بعد از اولین اجرا، listener به‌طور خودکار حذف می‌شود. مفید برای رویدادهایی که منطقاً فقط یک بار باید اجرا شوند — مثل «اولین بار کلیک روی این بنر».

passive: برای رویدادهای اسکرول و لمسی

window.addEventListener("scroll", handleScroll, { passive: true });
window.addEventListener("touchmove", handleTouch, { passive: true });

ویژگی passive: true به مرورگر می‌گوید که listener شما قصد preventDefault ندارد — بنابراین مرورگر می‌تواند اسکرول را روان‌تر انجام دهد. در پروژه‌های واقعی، فراموش‌کردن این تنظیم روی scroll و touchmove منبع اصلی لَگ اسکرول در موبایل است.

capture: فاز Capture

parent.addEventListener("click", handleClick, { capture: true });

اجرای listener در فاز Capture به‌جای Bubbling. در پروژه‌های واقعی، به‌ندرت لازم می‌شود، ولی در بعضی سناریوهای پیشرفته (مثلاً گرفتن رویداد قبل از فرزندان) مفید است.

کارایی: debounce، throttle و passive

یکی از بزرگ‌ترین دشمنان کارایی در رویدادها، اجرای سریع و مکرر کد است. سه رویداد که در پروژه‌های واقعی مشکل‌ساز هستند: scroll، resize و input. راه‌حل: دو الگوی کلاسیک:

Debounce: تأخیر بعد از توقف

function debounce(fn, delay) {
    let timeoutId;

    return function (...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => fn.apply(this, args), delay);
    };
}

const search = debounce((query) => {
    console.log("Searching for:", query);
}, 300);

input.addEventListener("input", (event) => search(event.target.value));

Debounce کد را بعد از توقف ورودی اجرا می‌کند. مناسب جستجوی زنده — کاربر تا وقتی تایپ می‌کند، هیچ درخواستی ارسال نمی‌شود.

Throttle: اجرای حداکثر با فاصله‌ی مشخص

function throttle(fn, limit) {
    let inThrottle;

    return function (...args) {
        if (!inThrottle) {
            fn.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

const handleScroll = throttle(() => {
    console.log("Scrolling...");
}, 200);

window.addEventListener("scroll", handleScroll, { passive: true });

Throttle کد را حداکثر یک‌بار در بازه‌ی مشخص اجرا می‌کند. مناسب اسکرول و resize — که کاربر دائماً تکرارشان می‌کند ولی شما نمی‌خواهید در هر فریم اجرا شوید.

این دو الگو، پایه‌ی بهینه‌سازی رویدادها در پروژه‌های واقعی هستند. اصول کامل این بهینه‌سازی‌ها در بهینه‌سازی جاوااسکریپت آمده است.

رویداد، مثل ضربه‌ی قلب است — اگر در هر ضربه اجرا شوید، خسته می‌شوید؛ اگر با فاصله‌ی منظم گوش بدهید، هم زنده‌اید هم مؤثر.

اشتباهاتی که در پروژه‌های واقعی دیده‌ام

در بازبینی کد پروژه‌های مختلف، این اشتباهات را زیاد دیده‌ام:

  • استفاده از HTML attribute برای اتصال رویداد: onclick="..." در HTML، منطق را از جاوااسکریپت جدا می‌کند و نگهداری را سخت. همیشه addEventListener استفاده کنید.
  • عدم حذف listener: در پروژه‌های SPA، وقتی کامپوننت حذف می‌شود، listenerهای آن هم باید حذف شوند. بدون آن، نشت حافظه و رفتار غیرمنتظره.
  • arrow function بی‌نام در removeEventListener: تابع بی‌نام، reference متفاوتی دارد و قابل حذف نیست. همیشه تابع نام‌دار بدهید.
  • استفاده‌ی بی‌دلیل از stopPropagation: این متد، Event Delegation را می‌شکند. فقط در موارد خاص استفاده کنید.
  • نادیده‌گرفتن debounce در رویداد input: جستجوی زنده بدون debounce، به سرور فشار می‌آورد و تجربه را کند می‌کند.
  • نادیده‌گرفتن passive در رویدادهای scroll: اسکرول لَگ‌دار در موبایل، معمولاً نتیجه‌ی همین اشتباه است.
  • مقایسه‌ی event.key با keyCode: keyCode قدیمی و متناقض است. event.key استاندارد امروز است.
  • attach listener به عناصر حذف‌شده: وقتی عنصر از DOM حذف می‌شود، listener آن هم باید حذف شود. بدون آن، نشت حافظه در پروژه‌های طولانی‌مدت.
  • عدم پیش‌بینی touch در پروژه‌های موبایل: رویدادهای click گاهی در موبایل با تاخیر ۳۰۰ میلی‌ثانیه‌ای اجرا می‌شوند. برای UX بهتر، از Pointer Events استفاده کنید.
  • استفاده از mouseover به‌جای mouseenter: تفاوت subtle ولی مهم — mouseover روی فرزندان Bubble می‌کند و می‌تواند رفتار ناخواسته بسازد.
  • فراموش کردن preventDefault در فرم: باعث رفرش صفحه و از دست رفتن حالت. در فرم‌های SPA همیشه لازم است.
  • ترکیب منطق درون listener: listenerهای پیچیده، خوانایی را پایین می‌آورند. منطق را در توابع مجزا بگذارید و listener فقط آن‌ها را صدا بزند.
  • مقایسه‌ی مقادیر با رشته در رویدادهای form: event.target.value همیشه رشته است. برای مقایسه‌ی عددی، اول تبدیل کنید.
  • نبود مستندسازی Custom Events: اگر از Custom Events استفاده می‌کنید، فهرستشان را مستند کنید. دو ماه بعد، پیدا کردن «کدام کامپوننت به کدام رویداد گوش می‌دهد» بدون مستندات، ساعت‌ها وقت می‌گیرد.
  • قربانی‌کردن خوانایی برای کوتاهی: listenerهای یک‌خطی با زنجیره‌ی طولانی، در دیباگ به کابوس تبدیل می‌شوند. ارزش خوانایی از کوتاهی بیشتر است.

یک توصیه‌ی عملی از تجربه: در پروژه‌های جدید، یک الگوی ثابت برای مدیریت رویدادها تعریف کنید. مثلاً همه‌ی listenerها را در یک فایل متمرکز کنید، یا از یک ساختار مشترک استفاده کنید که همه‌ی listenerها را با تابع نام‌دار مدیریت می‌کند. این انضباط کوچک، در دیباگ و نگهداری، ساعت‌ها وقت شما را ذخیره می‌کند. اگر با ES6 آشنا نیستید، آموزش ES6 در جاوااسکریپت پیش‌نیاز خوبی است — چون خیلی از الگوهای مدرن رویدادها، به امکانات ES6 تکیه دارند. و اگر روی پروژه‌های React یا Vue کار می‌کنید، اصول رویدادها همان است ولی با یک لایه‌ی انتزاعی؛ در مفاهیم پایه جاوااسکریپت این تفاوت را توضیح داده‌ام.

سخن آخر

رویدادها در جاوااسکریپت، از یک addEventListener ساده شروع می‌شوند ولی در پروژه‌های واقعی، به یک مدل ارتباطی تبدیل می‌شوند که روانی و پایداری رابط کاربری را تعیین می‌کند. سه نکته‌ی اصلی که در این مقاله به آن‌ها رسیدیم: اول، مسیر جریان رویداد (Bubbling و Capture) را بشناسید — بدون آن، نمی‌دانید کدتان چرا در جای اشتباه اجرا می‌شود؛ دوم، Event Delegation الگوی طلایی لیست‌ها است — یک listener روی والد به‌جای صدها listener روی فرزندان، هم سریع‌تر است و هم پویا؛ سوم، بهینه‌سازی با debounce، throttle و passive، تفاوت بین یک اپلیکیشن روان و یک اپلیکیشن لَگ‌دار را می‌سازد — مخصوصاً در رویدادهای scroll و input.

اگر امروز می‌خواهید در رویدادها ماهر شوید، سه کار کوچک پیشنهاد می‌کنم: یک مودال ساده بسازید که با کلیک بیرون بسته شود ولی کلیک داخل آن کاری نکند — این تمرین تفاوت target و currentTarget را زنده می‌کند؛ یک لیست پویا بسازید که با Event Delegation مدیریت شود و عنصر جدید هم خودکار پوشش داده شود؛ و یک جستجوی زنده پیاده کنید که با debounce سرور را از فشار نجات دهد. همین سه تمرین، ۹۰٪ مهارت‌های عملی رویدادها را در ذهن شما زنده می‌کند. اگر تجربه‌ای از کار با رویدادها در پروژه‌های خودتان دارید — مخصوصاً اگر با یک باگ Bubbling یا مشکل کارایی روبرو شده‌اید — در دیدگاه‌ها بنویسید؛ همین نکته‌های میدانی، برای خواننده‌ی بعدی از هر مستند رسمی ارزشمندتر است. 🎯