نکات تکمیلی در رابطه با رویدادها
حذف Event Listener ها
پس از این که یک Event Listener برای یک عنصر خاص تعریف شد. در هر نقطهای از برنامه این امکان وجود دارد تا آن را حذف کنیم. برای حذف یک Event Listener میتوان از متد removeEventListener استفاده کرد. آرگومانهای ورودی این متد دقیقاً مانند آرگومانهای ورودی متد addEventListener هستند. یعنی همان ورودیهایی که برای ایجاد Event Listener به متد addEventListener ارسال شدهاند را باید به متد removeEventListener ارسال کرد، تا Event Listener تعریف شده حذف شود.
یکی از کاربردهای معمول حذف Event Listener ها، تعیین یک تابع Event Handler برای یک عنصر است، به طوری که فقط یک بار اجرا شود. مثلاً در برنامهی زیر، پس از اولین کلیک بر روی دکمه، پیام "Clicked" به کاربر نمایش داده میشود. اما با توجه به استفاده از متد removeEventListener در تابع handler، در کلیکهای بعدی هیچ اتفاقی نمیافتد. زیرا Event Listener تعیین شده برای عنصر <button> حذف شده است.
const button = document.querySelector('button');
button.addEventListener('click' , handler);
function handler(){
alert('Clicked');
button.removeEventListener('click' , handler);
}
نکته : در صورت استفاده از توابع بینام یا Arrow Function ها به عنوان آرگومان دوم متد addEventListener، امکان استفاده از متد removeEventListener برای حذف این Event Listener ها وجود ندارد. زیرا متد removeEventListener به نام تابع Event Handler احتیاج دارد.
لغو رفتار پیشفرض رویدادها
بسیاری از رویدادها در صفحات وب، دارای رفتارهایی از پیش تعیین شده هستند. یعنی در صورت وقوع آن رویداد، عکسالعمل خاصی انجام میشود. و برای انجام این عکسالعمل نیازی به تعریف یک Event Handler نیست. یعنی مرورگر به صورت پیشفرض Event Handler هایی برای مدیریت این نوع رویدادها تعریف کرده است.
مثلاً با کلیک کردن بر روی یک لینک، آدرس صفحهی وب تغییر کرده و صفحهی جدیدی در مرورگر بارگذاری خواهد شد. در واقع این پاسخ پیشفرض لینکها به رویداد click است. یا مثلاً با کلیک کردن روی دکمهی submit از یک فرم، اطلاعات فرم به آدرس تعیین شده در صفت action عنصر <form> ارسال میشوند. یا به عنوان مثالی دیگر، با کلیک راست کردن بر روی صفحهی وب، یک منو به کاربر نمایش داده میشود. موارد بسیاری از این نوع رفتارهای پیشفرض در برابر رویدادها، در صفحات وب وجود دارد.
اما در برخی مواقع لازم است تا از انجام این رفتارهای پیشفرض جلوگیری کنیم. مثلا با کلیک کردن روی یک لینک، آدرس صفحه تغییر نکند و صفحهی جدیدی بارگذاری نشود. یا در زمان کلیک کردن بر روی دکمهی submit، در صورت نامعتبر بودن اطلاعات وارد شده، از ارسال فرم جلوگیری شود. در چنین شرایطی میتوان از متد preventDefault از شئ event استفاده کرد.
فراخوانی متد preventDefault در یک Event Handler، از انجام رفتار پیشفرض رویداد، جلوگیری میکند. البته برای تمام رویدادها نمیتوان از این متد استفاده کرد. زیرا رفتار پیشفرض برخی رویدادها قابل لغو کردن نیست. مثلاً رفتار پیشفرض رویدادهایی مانند mousemove یا blur (در فصل بعد معرفی میشود) قابل لغو کردن نیست. اما رفتار پیشفرض رویدادهایی مانند click یا contextmenu و یا submit (در فصل بعد معرفی میشود) را میتوان لغو کرد.
شئ event دارای خاصیتی به نام cancelable است که نشان میدهد رویداد جاری قابل لغو کردن است یا خیر؟ همچنین شئ event خاصیتی به نام defaultPrevented دارد که نشان میدهد رویداد جاری لغو شده است یا خیر؟ یعنی مقدار این خاصیت در ابتدا false است. اما پس از فراخوانی متد preventDefault به true تغییر میکند.
مثال زیر نحوهی استفاده از متد preventDefault و خاصیتهای defaultPrevented و cancelable را نشان میدهد. در این مثال از یک لینک در صفحهی وب استفاده شده است که با کلیک کردن بر روی آن، با استفاده از متد preventDefault از تغییر آدرس صفحه جلوگیری میشود. همچنین مقدار خاصیتهای cancelable و defaultPrevented قبل و بعد از اجرای متد preventDefault به کاربر نمایش داده میشود.
const link = document.querySelector('a');
link.addEventListener('click' , handler);
function handler(event){
alert('cancelable: ' + event.cancelable);
alert('defaultPrevented: ' + event.defaultPrevented);
event.preventDefault();
alert('defaultPrevented: ' + event.defaultPrevented);
}
این مثال را میتوانید اینجا در CodePen اجرا کنید. با اجرای این برنامه و کلیک کردن بر روی لینک، مشاهده خواهید کرد که تغییر مسیر اتفاق نمیافتد. اما در صورتی که متد preventDefault را حذف کرده و دوباره برنامه را اجرا کنید، خواهید دید که با کلیک کردن روی لینک، آدرس صفحه تغییر میکند و به صفحهی اصلی سایت OTedia منتقل خواهید شد.
آشنایی با Event Delegation
فرض کنید در یک صفحهی وب تعداد زیادی عنصر مشابه وجود دارد. اگر قصد داشته باشیم برای یک رویداد خاص، یک تابع را به عنوان Event Handler برای تمام این عناصر تعیین کنیم، چه باید کرد؟ آیا لازم است تا برای تمام این عناصر یک بار از متد addEventListener استفاده شود؟
مثلاً یک عنصر <ul> را در نظر بگیرید که تعداد زیادی عنصر <li> درون آن قرار دارند. اگر قصد داشته باشیم برای رویداد click تمام <li> ها، از یک تابع خاص به عنوان Event Handler استفاده کنیم، با روشهایی که تا به حال معرفی شدهاند، باید برای تمام این عناصر یک بار از متد addEventListener استفاده کنیم.
این کار نه تنها به زمان زیادی نیاز دارد، بلکه حجم کدها را افزایش داده و خوانایی آن را نیز کاهش میدهد. اما یک مشکل بزرگ دیگر نیز در این روش وجود دارد. فرض کنید در حین اجرای برنامه، تعدادی عنصر <li> به عنوان فرزند به عنصر <ul> اضافه شوند. در این صورت باید برای این عناصر جدید نیز از متد addEventListener استفاده کرده و تابع مربوطه را به عنوان Event Handler برای آنها تعریف کنیم. یا حتی ممکن است خاصیت innerHTML از عنصر <ul> تغییر داده شود. در این صورت تمام Event Listener هایی که برای فرزندان این عنصر تعریف شده بودند، از بین خواهند رفت. و باید مجدداً برای فرزندان جدید، با استفاده از متد addEventListener تابع مربوطه را به عنوان Event Handler تعریف کنیم.
برای حل تمام این مشکلات میتوان از راه حلی به نام Event Delegation استفاده کرد. همانطور که در بخش قبلی دیدیم، هر رویدادی که برای یکی از عناصر DOM رخ میدهد، به ترتیب برای تمام عناصر والد آن عنصر تا رسیدن به شئ document رخ خواهد داد. یعنی هر رویدادی که برای هر یک از عناصر صفحهی وب رخ میدهد، برای شئ document نیز رخ میدهد. در نتیجه میتوان فقط یک بار با متد addEventListener یک تابع را به عنوان Event Handler برای شئ document تعریف کرد، تا یک رویداد خاص را برای این شئ مدیریت کند. از این به بعد وقوع این رویداد خاص در هر بخشی از صفحهی وب، منجر به اجرای همان تابع Event Handler خواهد شد. به این روش در تعریف Event Listener ها اصطلاحاً Event Delegation (نمایندگی رویداد) گفته میشود.
البته همیشه لازم نیست تا Event Listener را برای شئ document تعریف کنیم. زیرا با توجه به ساختار Event Flow که در بخش قبلی توضیح داده شد، تعداد رویدادهای شئ document در یک صفحهی وب از تمام عناصر بیشتر است. در نتیجه هرچه تعداد Event Listener های این شئ کمتر باشد، سرعت و کارایی برنامه بهتر خواهد بود.
روش اصولی و صحیح این است که از عنصری برای Event Delegation استفاده شود که والد تمام عناصر هدف باشد. مثلاً در مورد عناصر <li> میتوان از عنصر <ul> برای Event Delegation استفاده کرد. زیرا تمام رویدادهایی که برای <li> ها رخ میدهند، برای عنصر <ul> نیز رخ میدهند. در صورت نیاز به تشخیص عنصری که رویداد را تولید کرده است نیز میتوان از خاصیت target شئ event استفاده کرد. مثال زیر نحوهی استفاده از Event Delegation را نشان میدهد.
const list = document.querySelector('ul');
list.addEventListener('click' , handler);
function handler(event){
alert(event.target.textContent);
}
این مثال را میتوانید اینجا اجرا کنید. در این مثال با کلیک کردن بر روی هر یک از <li> ها، تابع handler فراخوانی شده و با استفاده از خاصیت textContent، محتوای متنی عنصری که رویداد را تولید کرده است به کاربر نمایش داده میشود. همچنین اگر به هر طریقی <li> های جدیدی به عنصر <ul> اضافه شوند، رفتار برنامه با عناصر جدید نیز کاملاً مشابه خواهد بود. به این نوع Event Handler ها که با عناصری که بعداً به وجود میآیند نیز رفتار یکسانی دارند، اصطلاحاً Event Handler های زنده یا Live Event Handler گفته میشود.
رویداد DOMContentLoaded
اگر به یاد داشته باشید در فصل قبل به این نکته اشاره شد که انتخاب عناصر صفحهی وب، باید زمانی انجام شود که عناصر مورد نظر در مرورگر بارگذاری شده باشند. به همین دلیل معمولاً کدهای جاوا اسکریپت (داخلی یا خارجی) در انتهای تگ <body> قرار داده میشوند تا در زمان اجرای این کدها، تمام عناصر صفحهی وب بارگذاری شده باشند.
به عنوان مثال برنامهی زیر به درستی اجرا نخواهد شد و با خطا مواجه میشود. زیرا کدهای جاوا اسکریپت در بخش <head> و قبل از بارگذاری تگ <body> نوشته شدهاند. و با توجه به این که کدهای یک صفحهی وب از بالا به پایین بارگذاری و اجرا میشوند، در زمان اجرای این دستورات، هنوز عنصر <body> به ساختار DOM اضافه نشده است.
<html>
<head>
<title>Example</title>
<script>
document.body.innerHTML = 'Document Loaded';
alert('Body content changed!');
</script>
</head>
<body>
</body>
</html>
توجه کنید که از دو دستور فوق، فقط دستور اول خطا تولید میکند. زیرا دسترسی به عنصر <body> قبل از بارگذاری آن ممکن نیست. اما دستور دوم نیز اجرا نخواهد شد. زیرا با بروز اولین خطا در برنامه، اجرای برنامه متوقف شده و سایر دستورات نیز اجرا نخواهند شد.
اما اگر همین دستورات را در یک تابع قرار دهیم و آن تابع را بعد از بارگذاری کامل صفحه فراخوانی کنیم، حتی با قرار دادن این تابع در بخش <head>، خطایی به وجود نخواهد آمد. زیرا دستورات پس از بارگذاری تگ <body> اجرا میشوند. حال سوال این است که این تابع را چه زمانی فراخوانی کنیم؟ اگر این تابع را نیز مانند دستورات فوق در بخش <head> و قبل از بارگذاری تگ <body> فراخوانی کنیم، همان مشکل قبلی به وجود خواهد آمد.
راه حل این مشکل رویدادی به نام DOMContentLoaded است. این رویداد پس از بارگذاری کامل سند HTML رخ میدهد. پس میتوان تابع اشاره شده را به عنوان Event Handler برای این رویداد تعریف کرد. در این صورت پس از بارگذاری کامل سند HTML، این تابع اجرا شده و دستورات داخل آن نیز بدون هیچ مشکلی اجرا خواهند شد.
با توجه به توضیحات ارائه شده، مثال قبل را میتوان به این صورت اصلاح کرد.
<html>
<head>
<title>Example</title>
<script>
document.addEventListener('DOMContentLoaded' , handler);
function handler(){
document.body.innerHTML = 'Document Loaded';
alert('Body content changed!');
}
</script>
</head>
<body>
</body>
</html>
توجه کنید که این مثال و حالت قبلی آن در CodePen به درستی قابل اجرا نیستند. زیرا در CodePen تگهای <body> و <head> به صورت خودکار ایجاد میشوند و فقط محتویات تگ <body> توسط کاربر وارد میشود. به همین دلیل برای آزمایش کردن دو حالت مختلف این مثال، باید آنها را در یک فایل HTML ذخیره کرده و اجرا کنید.
البته معمولاً ترجیح داده میشود که کدهای جاوا اسکریپت در انتهای تگ <body> قرار داده شوند. زیرا این کار علاوه بر این که بدون نیاز به رویداد DOMContentLoaded مشکل فوق را حل میکند، موجب بارگذاری سریعتر صفحهی وب نیز میشود. زیرا در این حالت ابتدا کدهای HTML که تشکیل دهندهی ساختار اصلی صفحهی وب هستند بارگذاری میشوند. سپس کدهای جاوا اسکریپت بارگذاری میشوند. اما با این حال برخی برنامهنویسان ترجیح میدهند کدهای جاوا اسکریپت را در بخش <head> قرار دهند. در این صورت باید از رویداد DOMContentLoaded برای جلوگیری از بروز خطا استفاده کرد.
تقریباً تمام مباحث عمومی مرتبط با رویدادها در این فصل مطرح شدند. البته تعداد زیادی از رویدادهایی که در صفحات وب به کار برده میشوند هنوز معرفی نشدهاند. در فصلهای بعدی انواع دیگری از رویدادها معرفی، و کاربرد هر یک نشان داده خواهد شد. اما اصول کار با تمام موارد بعدی نیز به همان شکلی است که در این فصل ارائه شد.