۵۰ درصد تخفیف ویژه برای همه دوره‌ها از شنبه

هنگامی که ما از زبان برنامه نویسی جاوااسکریپت برای برنامه‌های خود استفاده می‌کنیم تقریبا در هر پروژه‌ای با event listenerها در ارتباط هستیم. این event listenerها در ابتدا ساده به نظر می‌رسند اما تعداد زیادی ویژگی کم‌تر شناخته شده مانند bubbling، capture، delegation و غیره دارند که ممکن است کمی پیچیده‌ باشد. درک این ویژگی‌ها نقش مهمی برای تبدیل شدن به یک توسعه‌دهنده متخصص جاوااسکریپت دارند. در این مقاله سعی داریم تا آن‌ها را باهم بررسی کنیم.

مفاهیم اولیه Event Listenerها

event listener در جاوااسکریپت راهی است که به کمک آن می‌توانیم باعث ایجاد تعامل با کاربر شویم و هر زمان که آن عمل انجام شد، کدی را اجرا کنیم. یکی از موارد استفاده رایج برای event listenerها کلیک روی یک دکمه است.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const button = document.querySelector("button")
button.addEventListener("click", e => {
console.log(e)
})
const button = document.querySelector("button") button.addEventListener("click", e => { console.log(e) })
const button = document.querySelector("button")
button.addEventListener("click", e => {
  console.log(e)
})

برای تنظیم یک event listener باید یک متغیر داشته باشیم که به یک المنت ارجاع دارد. سپس متد

addEventListener
addEventListenerرا در آن المنت فراخوانی می‌کنیم. این تابع حداقل دو پارامتر را دریافت می‌کند.

پارامتر اول یک رشته است و نام eventای که می‌خواهیم از آن استفاده کنیم را مشخص می‌کند. صدها event وجود دارد که می‌توانیم آن‌ها را در پروژه‌های خود به کار بگیریم مانند

click
click،
input
inputو
mousemove
mousemove. در این لینک لیست کاملی از همه eventها وجود دارد اما ما فقط به تعداد انگشت‌شماری از آن‌ها نیاز خواهیم داشت. بنابراین لازم نیست تا آن‌ها را به خاطر بسپاریم.

پارامتر دوم تابعی است که یک آرگومان دارد و آن آرگومان event می‌باشد و معمولا

e
eنامیده می‌شود. این تابع هر بار که event رخ می‌دهد فراخوانی می‌شود و آبجکت event حاوی اطلاعات مربوط به آن است. بسته به اینکه از چه eventای استفاده می‌کنیم آبجکت آن دارای ویژگی‌های متفاوتی خواهد بود که همگی آن‌ها مهم هستند اما تقریباً هر eventای دارای یک ویژگی
target
targetمی‌باشد. این ویژگی نشان‌دهنده المنتی است که event روی آن رخ می‌دهد که برای استفاده‌های پیشرفته‌تر از event listener ها مهم است که در ادامه مقاله به آن خواهیم پرداخت.

باید به این موضوع مهم توجه داشته باشیم که اگر چندین event listener روی یک المنت برای یک event داشته باشیم، همه آن‌ها به ترتیبی که به المنت اضافه شده‌اند، فعال می‌گردند.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
button.addEventListener("click", e => {
console.log("This runs first")
})
button.addEventListener("click", e => {
console.log("This runs second")
})
button.addEventListener("click", e => { console.log("This runs first") }) button.addEventListener("click", e => { console.log("This runs second") })
button.addEventListener("click", e => {
  console.log("This runs first")
})

button.addEventListener("click", e => {
  console.log("This runs second")
})

انتشار Event

تا این قسمت با مفاهیم اولیه مربوط به event listenerها آشنا شدیم اما وقتی شروع به ایجاد پروژه‌های پیشرفته‌تر می‌کنیم، باید بدانیم این eventها چگونه راه‌اندازی می‌شوند و چگونه از طریق DOM منتشر می‌گردند. اینجاست که مراحل bubble و capture مطرح می‌شوند.

تصور کنید کد HTML و جاوااسکریپت زیر را داریم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<div class="parent">
<div class="child"></div>
</div>
<div class="parent"> <div class="child"></div> </div>
<div class="parent">
  <div class="child"></div>
</div>
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
parent.addEventListener("click", () => {
console.log("Parent")
})
child.addEventListener("click", () => {
console.log("Child")
})
parent.addEventListener("click", () => { console.log("Parent") }) child.addEventListener("click", () => { console.log("Child") })
parent.addEventListener("click", () => {
  console.log("Parent")
})

child.addEventListener("click", () => {
  console.log("Child")
})

اگر روی المنت child کلیک کنیم، احتمالا فکر می‌کنید که در کنسول

child
child را ثبت می‌کند، اما در واقع
child
child و
parent
parentرا به ترتیب وارد می‌کند. دلیل این امر bubbling  می‌باشد.

فاز Bubble

هنگامی که یک event بر روی المنتی فعال می‌شود آن event در درخت document بر روی تمام المنت‌هایی که المنت مورد نظر ما در داخل آن‌ها قرار دارد، اعمال می‌شود. در مثالی که داریم وقتی روی child کلیک می‌کنیم، event listener کلیک روی المنت parent نیز فعال می‌شود، زیرا child داخل المنت parent است. این حتی یک گام فراتر می‌رود و event listener کلیک را در خود document نیز فعال می‌کند. ما از این موضوع هنگام مواجه شدن با مجموعه eventها استفاده خواهیم کرد.

فاز Capture

همه مفاهیمی که در بخش قبلی با آن‌ها آشنا شدیم مربوط به فاز 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
addEventListenerآشنا شویم.

این پارامتر سوم یک آبجکت است که دارای ویژگی

capture
captureمی‌باشد که وقتی روی true تنظیم شود event مورد نظر را به عنوان یک capture event برچسب‌گذاری می‌کند.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 })
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 })
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 کلیک کنیم خروجی زیر را خواهیم داشت:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Parent Capture
Child Capture
Child Bubble
Parent Bubble
Parent Capture Child Capture Child Bubble Parent Bubble
Parent Capture
Child Capture
Child Bubble
Parent Bubble

توقف انتشار Event

شاید داشتن این دو فاز از eventها عجیب به نظر برسد اما دلیل آن این است که بتوانیم به همان ترتیبی که نیاز داریم به eventها پاسخ دهیم. یکی از موارد استفاده رایج برای فاز capture این است که یک event را قبل از اینکه به childها برسد متوقف کنیم.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
parent.addEventListener("click", e => {
console.log("Parent Capture")
e.stopPropagation()
}, { capture: true })
child.addEventListener("click", () => {
console.log("Child Bubble")
})
parent.addEventListener("click", e => { console.log("Parent Capture") e.stopPropagation() }, { capture: true }) child.addEventListener("click", () => { console.log("Child Bubble") })
parent.addEventListener("click", e => {
  console.log("Parent Capture")
  e.stopPropagation()
}, { capture: true })

child.addEventListener("click", () => {
  console.log("Child Bubble")
})

با استفاده از متد

stopPropagation
stopPropagationبر روی آبجکت event، می‌توانیم event را از ادامه انجام فازهایی که درمورد آن‌ها صحبت کردیم بازداریم. به این معنی که اگر event listenerهای دیگری در زنجیره وجود داشته باشد که باید ایجاد شوند، آن‌ها را اجرا نمی‌کند. در مثال بالا، فقط
Parent Capture
Parent Captureدر کنسول نمایش داده می‌شود، زیرا ما از انتشار event پس از capture event listener مربوط به parent جلوگیری می‌کنیم.

روش دیگر استفاده از

stopImmediatePropagation
stopImmediatePropagationروی آبجکت event است که کمی متفاوت می‌باشد. اگر از متد
stopImmediatePropagation
stopImmediatePropagationاستفاده کنیم، در این صورت event نه تنها انتشار به المنت‌های child و یا parent را از طریق فازهای bubble و Capture متوقف خواهد کرد بلکه از فعال شدن سایر eventها بر روی المنت مورد نظر نیز جلوگیری می‌کند.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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")
})
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") })
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
stopImmediatePropagationاستفاده کردیم، سایر event listenerهای کلیک روی المنت parent نیز فعال نمی‌شوند. نکته مهمی که باید به آن توجه داشته باشیم این است که event listenerها در همان المنت به ترتیبی که تعریف شده‌اند فعال می‌شوند. بنابراین اگر می‌خواهیم با این روش از فعال شدن سایر event listenerها جلوگیری کنیم باید آن‌ها را بعد از listenerای که انتشار را متوقف می‌کند، تعریف کنیم.

نکات مهم درمورد Bubbling

یکی از نکات مهمی که باید در مورد فاز bubbling مربوط به eventها بدانیم این است که همه آن‌ها وارد این فاز نمی‌شوند. Eventهایی مانند

focus
focusکه با فوکوس کردن روی یک المنت فعال می‌شود، وارد فاز bubble نمی‌شود.

حذف Event Listenerها

تا این قسمت مقاله با نحوه اضافه کردن event listenerها آشنا شدیم اما در نهایت باید بتوانیم listenerهایی که اضافه می‌کنیم را حذف کنیم. ساده‌ترین راه برای انجام این کار استفاده از متد

removeEventListener
removeEventListenerاست، اما دو راه پیشرفته‌تر نیز برای انجام این کار وجود دارد که در مورد آن‌ها نیز صحبت خواهیم کرد.

removeEventListener

متد

removeEventListener
removeEventListenerیک تابع ساده است که می‌توانیم روی یک المنت فراخوانی کرده و event listenerای را که قبلاً با استفاده از متد
addEventListener
addEventListenerاضافه کرده بودیم حذف نماییم.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
button.addEventListener("click", sayHi)
button.removeEventListener("click", sayHi)
function sayHi() {
console.log("Hi")
}
button.addEventListener("click", sayHi) button.removeEventListener("click", sayHi) function sayHi() { console.log("Hi") }
button.addEventListener("click", sayHi)
button.removeEventListener("click", sayHi)

function sayHi() {
  console.log("Hi")
}

کد بالا با یک کلیک listenerای اضافه می‌کند که تابع

sayHi
sayHiرا فراخوانی کرده و بلافاصله آن را حذف می‌کند. توجه به این نکته مهم است که هنگام افزودن و یا حذف event listenerها باید مطمئن شویم که عملکرد آن‌ها دقیقاً یکسان باشد. اگر بخواهیم کد خود را به صورت زیر بنویسیم، در واقع event listener را حذف نمی‌کنیم، زیرا این دو تابع متفاوت هستند حتی اگر کد مشابهی داشته باشند.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
button.addEventListener("click", () => {
console.log("Hi")
})
button.removeEventListener("click", () => {
console.log("Hi")
})
button.addEventListener("click", () => { console.log("Hi") }) button.removeEventListener("click", () => { console.log("Hi") })
button.addEventListener("click", () => {
  console.log("Hi")
})

button.removeEventListener("click", () => {
  console.log("Hi")
})

اجرای Eventها برای یک بار

گاهی مواقع لازم است eventای فقط برای یک بار اجرا شود. برای انجام این کار می‌توانیم از

removeEventListener
removeEventListenerاستفاده کنیم ولی نتیجه آن همیشه دقیق نیست و ممکن است کدی که داریم دچار مشکل شود. به همین دلیل است که پارامتر سوم برای
addEventListener
addEventListenerدارای ویژگی به نام
once
onceاست که وقتی روی true تنظیم شود اطمینان حاصل می‌کند که event listener ما فقط یک بار اجرا می‌شود.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
button.addEventListener("click", () => {
console.log("Clicked")
}, { once: true })
button.addEventListener("click", () => { console.log("Clicked") }, { once: true })
button.addEventListener("click", () => {
  console.log("Clicked")
}, { once: true })

در مثال بالا مهم نیست که چند بار روی دکمه کلیک می‌کنیم،

Clicked
Clickedفقط یک بار در کنسول نمایش داده می‌شود زیرا event listener پس از یک بار اجرا به طور خودکار حذف خواهد شد.

توقف Event Listenerها

آخرین راه برای حذف event listenerها استفاده از

AbortController
AbortControllerاست که کم‌تر رایج بوده اما می‌تواند بسیار مفید باشد. اگر می‌خواهیم event listenerای ایجاد کنیم که تا زمان برآورده شدن یک شراط خاص کار کند این ممکن است گزینه مناسبی به‌شمار بیاید.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
let count = 0
const controller = new AbortController()
button.addEventListener("click", () => {
count++
console.log(count)
if (count >= 3) {
controller.abort()
}
}, { signal: controller.signal })
let count = 0 const controller = new AbortController() button.addEventListener("click", () => { count++ console.log(count) if (count >= 3) { controller.abort() } }, { signal: controller.signal })
let count = 0
const controller = new AbortController()

button.addEventListener("click", () => {
  count++
  console.log(count)
  if (count >= 3) {
    controller.abort()
  }
}, { signal: controller.signal })

کد بالا ممکن است کمی گیج‌کننده به نظر برسد اما مرحله به مرحله آن را بررسی خواهیم کرد. ابتدا یک

AbortController
AbortControllerجدید ایجاد می‌کنیم. سپس ویژگی
signal
signalمربوط به
AbortController
AbortController را به ویژگی
signal
signal متد
addEventListener
addEventListenerخود منتقل می‌کنیم. این کار event listener ما را به آن
AbortController
AbortController متصل می‌کند بنابراین اگر آن را قطع کنیم، event listenerای که داریم حذف می‌شود. در نهایت متد
abort
abortرا در
AbortController
AbortController فراخوانی می‌کنیم که تعداد آن بیشتر یا مساوی ۳ باشد تا event listener را حذف کند.

اساساً روش کار یک

AbortController
AbortController به این صورت است ما ویژگی
signal
signal را به تابع
addEventListener
addEventListenerمی‌دهیم و سپس در هر زمانی که
abort
abort را در
AbortController
AbortController فراخوانی کنیم می‌توانیم event listener را حذف کنیم.

Event Delegation

تمام مفاهیمی که تاکنون درمورد آن‌ها صحبت کردیم به موضوع نهایی این مقاله که در مورد event delegation است، منتهی می‌شود. درک نحوه عملکرد این مفهوم بسیار مهم است.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const buttons = document.querySelectorAll("button")
buttons.forEach(button => {
button.addEventListener("click", () => {
console.log("Clicked Button")
})
})
const newButton = document.createElement("button")
document.body.append(newButton)
const buttons = document.querySelectorAll("button") buttons.forEach(button => { button.addEventListener("click", () => { console.log("Clicked Button") }) }) const newButton = document.createElement("button") document.body.append(newButton)
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 مورد نظر ما قبل از اجرای کد توسط المنت‌هایی که برایمان مهم هستند فعال شده است یا خیر.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
document.addEventListener("click", e => {
if (e.target.matches("button")) {
console.log("Clicked Button")
}
})
const newButton = document.createElement("button")
document.body.append(newButton)
document.addEventListener("click", e => { if (e.target.matches("button")) { console.log("Clicked Button") } }) const newButton = document.createElement("button") document.body.append(newButton)
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
buttonمطابقت دارد یا نه. این سلکتور که به
matches
matchesمی‌دهیم، فقط یک سلکتور CSS است، شبیه چیزی که به
querySelector
querySelectorارسال می‌کنیم.

با نوشتن کد خود به این صورت، اطمینان حاصل می‌کنیم که هر دکمه‌ای که در صفحه ما وجود دارد، حتی دکمه‌هایی که به تازگی اضافه شده‌اند، با کلیک کردن به درستی کار می‌کنند. با این حال اگر روی المنتی به غیر از دکمه کلیک کنیم

e.target.matches
e.target.matchesمقدار false را برمی‌گرداند، به این معنی که هیچ مقداری ثبت نخواهد شد.

به‌طور کلی زمانی که با افزودن المنت‌ها به صورت پویا سر و کار داریم، نوشتن یک event listener بر روی parent برای واگذاری آن event با رعایت معیارهای صحیح بسیار سودمند است.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 })
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 })
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ها ممکن است در ظاهر ساده به نظر برسند، اما در واقع عمق شگفت‌انگیزی در آن‌ها وجود دارد. درک این مفاهیم کمک می‌کند تا عمق مهارت‌ برنامه نویسی ما به خوبی ارتقا پیدا کند.