هنگامی که ما از زبان برنامه نویسی جاوااسکریپت برای برنامههای خود استفاده میکنیم تقریبا در هر پروژهای با event listenerها در ارتباط هستیم. این event listenerها در ابتدا ساده به نظر میرسند اما تعداد زیادی ویژگی کمتر شناخته شده مانند bubbling، capture، delegation و غیره دارند که ممکن است کمی پیچیده باشد. درک این ویژگیها نقش مهمی برای تبدیل شدن به یک توسعهدهنده متخصص جاوااسکریپت دارند. در این مقاله سعی داریم تا آنها را باهم بررسی کنیم.
event listener در جاوااسکریپت راهی است که به کمک آن میتوانیم باعث ایجاد تعامل با کاربر شویم و هر زمان که آن عمل انجام شد، کدی را اجرا کنیم. یکی از موارد استفاده رایج برای event listenerها کلیک روی یک دکمه است.
const button = document.querySelector("button") button.addEventListener("click", e => { console.log(e) })
برای تنظیم یک event listener باید یک متغیر داشته باشیم که به یک المنت ارجاع دارد. سپس متد addEventListener
را در آن المنت فراخوانی میکنیم. این تابع حداقل دو پارامتر را دریافت میکند.
پارامتر اول یک رشته است و نام eventای که میخواهیم از آن استفاده کنیم را مشخص میکند. صدها event وجود دارد که میتوانیم آنها را در پروژههای خود به کار بگیریم مانند click
، input
و mousemove
. در این لینک لیست کاملی از همه eventها وجود دارد اما ما فقط به تعداد انگشتشماری از آنها نیاز خواهیم داشت. بنابراین لازم نیست تا آنها را به خاطر بسپاریم.
پارامتر دوم تابعی است که یک آرگومان دارد و آن آرگومان event میباشد و معمولا e
نامیده میشود. این تابع هر بار که event رخ میدهد فراخوانی میشود و آبجکت event حاوی اطلاعات مربوط به آن است. بسته به اینکه از چه eventای استفاده میکنیم آبجکت آن دارای ویژگیهای متفاوتی خواهد بود که همگی آنها مهم هستند اما تقریباً هر eventای دارای یک ویژگی target
میباشد. این ویژگی نشاندهنده المنتی است که event روی آن رخ میدهد که برای استفادههای پیشرفتهتر از event listener ها مهم است که در ادامه مقاله به آن خواهیم پرداخت.
باید به این موضوع مهم توجه داشته باشیم که اگر چندین event listener روی یک المنت برای یک event داشته باشیم، همه آنها به ترتیبی که به المنت اضافه شدهاند، فعال میگردند.
button.addEventListener("click", e => { console.log("This runs first") }) button.addEventListener("click", e => { console.log("This runs second") })
تا این قسمت با مفاهیم اولیه مربوط به event listenerها آشنا شدیم اما وقتی شروع به ایجاد پروژههای پیشرفتهتر میکنیم، باید بدانیم این eventها چگونه راهاندازی میشوند و چگونه از طریق DOM منتشر میگردند. اینجاست که مراحل bubble و capture مطرح میشوند.
تصور کنید کد HTML و جاوااسکریپت زیر را داریم:
<div class="parent"> <div class="child"></div> </div>
parent.addEventListener("click", () => { console.log("Parent") }) child.addEventListener("click", () => { console.log("Child") })
اگر روی المنت child کلیک کنیم، احتمالا فکر میکنید که در کنسول child
را ثبت میکند، اما در واقع child
و parent
را به ترتیب وارد میکند. دلیل این امر bubbling میباشد.
هنگامی که یک event بر روی المنتی فعال میشود آن event در درخت document بر روی تمام المنتهایی که المنت مورد نظر ما در داخل آنها قرار دارد، اعمال میشود. در مثالی که داریم وقتی روی child کلیک میکنیم، event listener کلیک روی المنت parent نیز فعال میشود، زیرا child داخل المنت parent است. این حتی یک گام فراتر میرود و event listener کلیک را در خود document نیز فعال میکند. ما از این موضوع هنگام مواجه شدن با مجموعه eventها استفاده خواهیم کرد.
همه مفاهیمی که در بخش قبلی با آنها آشنا شدیم مربوط به فاز bubble است که یک فاز پیشفرض بهشمار میآید و event listenerها در آن ایجاد میشوند. اما eventها فاز دیگری به نام فاز capture دارند که ابتدا اتفاق میافتد. فاز capture درست مانند فاز bubble است، اما event از المنت سطح بالا، که در مثال ما document است شروع میشود و به سمت المنتهای داخلی حرکت میکند. این بدان معناست که اگر در مثالی که داریم روی المنت child کلیک کنیم، event listener را برای document ، سپس parent، سپس child فعال میکنیم. پس از آن وارد فاز bubble میشویم و در آن event listenerها را برای child، سپس parent و سپس document فعال میکنیم.
تاکنون ما فقط نحوه تنظیم event listenerها برای فاز bubble را بررسی کردیم. اگر بخواهیم درمورد capture صحبت کنیم باید با پارامتر سوم متد addEventListener
آشنا شویم.
این پارامتر سوم یک آبجکت است که دارای ویژگی capture
میباشد که وقتی روی true تنظیم شود event مورد نظر را به عنوان یک capture event برچسبگذاری میکند.
parent.addEventListener("click", () => { console.log("Parent Bubble") }) parent.addEventListener("click", () => { console.log("Parent Capture") }, { capture: true }) child.addEventListener("click", () => { console.log("Child Bubble") }) child.addEventListener("click", () => { console.log("Child Capture") }, { capture: true })
کد بالا را داریم. اکنون اگر روی المنت child کلیک کنیم خروجی زیر را خواهیم داشت:
Parent Capture Child Capture Child Bubble Parent Bubble
شاید داشتن این دو فاز از eventها عجیب به نظر برسد اما دلیل آن این است که بتوانیم به همان ترتیبی که نیاز داریم به eventها پاسخ دهیم. یکی از موارد استفاده رایج برای فاز capture این است که یک event را قبل از اینکه به childها برسد متوقف کنیم.
parent.addEventListener("click", e => { console.log("Parent Capture") e.stopPropagation() }, { capture: true }) child.addEventListener("click", () => { console.log("Child Bubble") })
با استفاده از متد stopPropagation
بر روی آبجکت event، میتوانیم event را از ادامه انجام فازهایی که درمورد آنها صحبت کردیم بازداریم. به این معنی که اگر event listenerهای دیگری در زنجیره وجود داشته باشد که باید ایجاد شوند، آنها را اجرا نمیکند. در مثال بالا، فقط Parent Capture
در کنسول نمایش داده میشود، زیرا ما از انتشار event پس از capture event listener مربوط به parent جلوگیری میکنیم.
روش دیگر استفاده از stopImmediatePropagation
روی آبجکت event است که کمی متفاوت میباشد. اگر از متد stopImmediatePropagation
استفاده کنیم، در این صورت event نه تنها انتشار به المنتهای child و یا parent را از طریق فازهای bubble و Capture متوقف خواهد کرد بلکه از فعال شدن سایر eventها بر روی المنت مورد نظر نیز جلوگیری میکند.
parent.addEventListener("click", e => { console.log("Parent Capture 1") e.stopImmediatePropagation() }, { capture: true }) parent.addEventListener("click", e => { console.log("Parent Capture 2") }, { capture: true }) child.addEventListener("click", () => { console.log("Child Bubble") })
در مثال بالا، انتشار را در اولین capture event listener مربوط به parent متوقف کردیم بنابراین از انتشار event به سایر المنتها از طریق فازهای bubble و Capture جلوگیری خواهد شد. همچنین به دلیل این که از متد stopImmediatePropagation
استفاده کردیم، سایر event listenerهای کلیک روی المنت parent نیز فعال نمیشوند. نکته مهمی که باید به آن توجه داشته باشیم این است که event listenerها در همان المنت به ترتیبی که تعریف شدهاند فعال میشوند. بنابراین اگر میخواهیم با این روش از فعال شدن سایر event listenerها جلوگیری کنیم باید آنها را بعد از listenerای که انتشار را متوقف میکند، تعریف کنیم.
یکی از نکات مهمی که باید در مورد فاز bubbling مربوط به eventها بدانیم این است که همه آنها وارد این فاز نمیشوند. Eventهایی مانند focus
که با فوکوس کردن روی یک المنت فعال میشود، وارد فاز bubble نمیشود.
تا این قسمت مقاله با نحوه اضافه کردن event listenerها آشنا شدیم اما در نهایت باید بتوانیم listenerهایی که اضافه میکنیم را حذف کنیم. سادهترین راه برای انجام این کار استفاده از متد removeEventListener
است، اما دو راه پیشرفتهتر نیز برای انجام این کار وجود دارد که در مورد آنها نیز صحبت خواهیم کرد.
متد removeEventListener
یک تابع ساده است که میتوانیم روی یک المنت فراخوانی کرده و event listenerای را که قبلاً با استفاده از متد addEventListener
اضافه کرده بودیم حذف نماییم.
button.addEventListener("click", sayHi) button.removeEventListener("click", sayHi) function sayHi() { console.log("Hi") }
کد بالا با یک کلیک listenerای اضافه میکند که تابع sayHi
را فراخوانی کرده و بلافاصله آن را حذف میکند. توجه به این نکته مهم است که هنگام افزودن و یا حذف event listenerها باید مطمئن شویم که عملکرد آنها دقیقاً یکسان باشد. اگر بخواهیم کد خود را به صورت زیر بنویسیم، در واقع event listener را حذف نمیکنیم، زیرا این دو تابع متفاوت هستند حتی اگر کد مشابهی داشته باشند.
button.addEventListener("click", () => { console.log("Hi") }) button.removeEventListener("click", () => { console.log("Hi") })
گاهی مواقع لازم است eventای فقط برای یک بار اجرا شود. برای انجام این کار میتوانیم از removeEventListener
استفاده کنیم ولی نتیجه آن همیشه دقیق نیست و ممکن است کدی که داریم دچار مشکل شود. به همین دلیل است که پارامتر سوم برای addEventListener
دارای ویژگی به نام once
است که وقتی روی true تنظیم شود اطمینان حاصل میکند که event listener ما فقط یک بار اجرا میشود.
button.addEventListener("click", () => { console.log("Clicked") }, { once: true })
در مثال بالا مهم نیست که چند بار روی دکمه کلیک میکنیم، Clicked
فقط یک بار در کنسول نمایش داده میشود زیرا event listener پس از یک بار اجرا به طور خودکار حذف خواهد شد.
آخرین راه برای حذف event listenerها استفاده از AbortController
است که کمتر رایج بوده اما میتواند بسیار مفید باشد. اگر میخواهیم event listenerای ایجاد کنیم که تا زمان برآورده شدن یک شراط خاص کار کند این ممکن است گزینه مناسبی بهشمار بیاید.
let count = 0 const controller = new AbortController() button.addEventListener("click", () => { count++ console.log(count) if (count >= 3) { controller.abort() } }, { signal: controller.signal })
کد بالا ممکن است کمی گیجکننده به نظر برسد اما مرحله به مرحله آن را بررسی خواهیم کرد. ابتدا یک AbortController
جدید ایجاد میکنیم. سپس ویژگی signal
مربوط به AbortController
را به ویژگی signal
متد addEventListener
خود منتقل میکنیم. این کار event listener ما را به آن AbortController
متصل میکند بنابراین اگر آن را قطع کنیم، event listenerای که داریم حذف میشود. در نهایت متد abort
را در AbortController
فراخوانی میکنیم که تعداد آن بیشتر یا مساوی ۳ باشد تا event listener را حذف کند.
اساساً روش کار یک AbortController
به این صورت است ما ویژگی signal
را به تابع addEventListener
میدهیم و سپس در هر زمانی که abort
را در AbortController
فراخوانی کنیم میتوانیم event listener را حذف کنیم.
تمام مفاهیمی که تاکنون درمورد آنها صحبت کردیم به موضوع نهایی این مقاله که در مورد event delegation است، منتهی میشود. درک نحوه عملکرد این مفهوم بسیار مهم است.
const buttons = document.querySelectorAll("button") buttons.forEach(button => { button.addEventListener("click", () => { console.log("Clicked Button") }) }) const newButton = document.createElement("button") document.body.append(newButton)
در کد بالا، ما همه دکمههایی که در صفحه وجود دارد را انتخاب میکنیم و یک event listener مربوط به کلیک اضافه میکنیم. پس از آن یک دکمه جدید به صفحه اضافه میکنیم. این دکمه جدید به هیچ event listener کلیکی متصل نیست زیرا پس از اضافه شدن event listener به صفحه افزوده شده است. این یک اشتباه رایج است که ممکن است توسعهدهندگان تازهکار مرتکب آن میشوند. زیرا فکر میکنند این دکمه جدید نیز event listener را خواهد داشت، اما این چنین نیست. برای حل این مشکل باید هر بار که المنتهای جدید ایجاد میشوند event listenerها را نیز بهصورت دستی به آنها اضافه کرده و یا اینکه از event delegation استفاده کنیم.
با استفاده از event delegation ما event listener را روی یک المنت parent، مانند document تنظیم میکنیم. سپس در داخل آن المنت parent بررسی میکنیم که آیا event مورد نظر ما قبل از اجرای کد توسط المنتهایی که برایمان مهم هستند فعال شده است یا خیر.
document.addEventListener("click", e => { if (e.target.matches("button")) { console.log("Clicked Button") } }) const newButton = document.createElement("button") document.body.append(newButton)
از آنجایی که event کلیک تا المنتهای parent بالا میرود، میدانیم که در نهایت هر event کلیکی در صفحه ما به document راه پیدا میکند. سپس document را بررسی میکنیم تا ببینیم آیا هدف event با سلکتور button
مطابقت دارد یا نه. این سلکتور که به matches
میدهیم، فقط یک سلکتور CSS است، شبیه چیزی که به querySelector
ارسال میکنیم.
با نوشتن کد خود به این صورت، اطمینان حاصل میکنیم که هر دکمهای که در صفحه ما وجود دارد، حتی دکمههایی که به تازگی اضافه شدهاند، با کلیک کردن به درستی کار میکنند. با این حال اگر روی المنتی به غیر از دکمه کلیک کنیم e.target.matches
مقدار false را برمیگرداند، به این معنی که هیچ مقداری ثبت نخواهد شد.
بهطور کلی زمانی که با افزودن المنتها به صورت پویا سر و کار داریم، نوشتن یک event listener بر روی parent برای واگذاری آن event با رعایت معیارهای صحیح بسیار سودمند است.
function addGlobalEventListener(type, selector, callback, options) { document.addEventListener(type, e => { if (e.target.matches(selector)) callback(e) }, options) } addGlobalEventListener("click", ".btn", () => { console.log("Clicked Button") }, { once: true })
در حالی که استفاده از event listenerها ممکن است در ظاهر ساده به نظر برسند، اما در واقع عمق شگفتانگیزی در آنها وجود دارد. درک این مفاهیم کمک میکند تا عمق مهارت برنامه نویسی ما به خوبی ارتقا پیدا کند.