عنوان:

‫بررسی مفهوم Event bubbling در جی کوئری و تاثیر آن بر کارآیی کدهای نوشته شده


نویسنده: علی سالمیان
تاریخ: ۱۳۹۴/۰۸/۲۷ ۱۱:۱۰
آدرس: www.dntips.ir
Event bubbling یا جوشیدن رویدادها به مفهوم انتقال رویدادهای رخ داده در یک المنت به سمت المنت یا المنت‌های والد می‌باشد. برای مثال با کلیک بر روی یک المنت در صفحه، رویداد کلیک هم در همان المنت اجرا خواهد شد و هم در المنت‌های والد.
ساختار سند زیر را در نظر بگیرید:
<div id="parent">
    <div id="child1">
        <div id="child2">
            <div id="child3"></div>
        </div>
    </div>
</div>
حال اگر برای هرکدام از divهای موجود در سند، یک هندلر برای مدیریت رویداد کلیک نوشته شود و کاربر بر روی child3 کلیک کند، به ترتیب ابتدا رویداد مربوط به المنت child3 سپس child2 سپس child1 و در نهایت parent اجرا خواهد شد. یعنی با کلیک بر روی child3، تمامی هندلرهای کلیک اجرا خواهند شد. دلیل اینکار همان مفهوم Event bubbling است.
Event bubbling فقط مختص صفحات وب نیست؛ بلکه در تمامی سیستم عامل‌ها یکی از مفاهیم مدیریت رخدادها(Events) است. حتی در برنامه‌های مبتنی بر ویندوز فرم هم شما با این مفهوم برخورد کرده‌اید.
در صفحات وب، در نهایت رویدادها به شیء Window منتقل می‌شوند و در یک وب فرم، به From اصلی برنامه.
حال با این مقدمه به سراغ بهینه سازی کدهای نوشته شده‌ی خود می‌رویم. اگر از کتابخانه‌ی جی‌کوئری استفاده کرده باشید، حتما از رویدادهای مختلف ماوس و صفحه کلید بهره برده‌اید. تصور برنامه‌ای که از رویدادها استفاده نکند و باید با کاربر در تعامل باشد، غیرممکن است؛ زیرا این رویدادها هستند که درخواست‌های کاربر را به برنامه منتقل می‌کنند.
به قطعه کد زیر توجه کنید:
$('#parent').on('click', function (event) {

});

$('#child1').on('click', function (event) {

});

$('#child2').on('click', function (event) {

});
ما یک هندلر برای مدیریت رویداد کلیک المنت parent نوشته‌ایم؛ یکی برای المنت child1 و یکی دیگر برای child2. با استفاده از مفهوم جوشیدن رخدادها می‌توانیم هر سه هندلر را حذف و به یک هندلر تبدیل کنیم!
$(document).on('click', '#parent, #child1, #child2, #child3', function (event) {

});
شاید بپرسید مزیت اینکار چیست؟ نکته‌ی کلیدی در همینجاست. میزان حافظه‌ی مصرفی مدیریت یک رخداد، به مراتب کمتر از چندین رخداد است.
در واقع شما فقط یک هندلر را ثبت و تمامی کارهای لازم را به آن می‌سپارید. شدیدا توصیه می‌شود که در نوشتن کدهای خود از ایجاد هندلر بر روی هر عنصر خودداری کنید.
برای مثال اگر شما در صفحه‌ی مدیریت پست‌ها قرار دارید و برای ویرایش هر پست دکمه‌ای را تعیین کرده باشید به جای نوشتن کدی مانند زیر:
$('.post .edit').on('click', function (event) {

});
از نسخه‌ی بهینه شده‌ی آن استفاده کنید:
$(document).on('click', '.post .edit', function (event) {

});
تصور کنید شما در همین صفحه 50 پست را به کاربر نشان داده باشید و اگر از کد بالا استفاده کنید، به ازای هر 50 دکمه‌ی ویرایش، یک هندلر برای رویداد کلیک خواهید داشت. حالا اگر از کد پایین استفاده کنید، تنها یک هندلر برای 50 رویداد خواهید داشت.
همان صفحه‌ی مدیریت پست را در نظر بگیرید. 50 پست داریم. هر کدام یک دکمه‌ی ویرایش، حذف، امتیازات، کامنتها و کلی ابزار دیگر که همه با رویداد کلیک فعال می‌شوند. چیزی حدود به 300 رویداد را باید ثبت کنید!
این واقعا یک تراژدی بزرگ در مصرف حافظه محسوب می‌شود. پس بهینه‌تر است تا با نوشتن یک رویداد کلیک روی کل شیء سند، از ایجاد هندلرهای اضافی خودداری کنید.

در اینجا دو نکته قابل ذکر است:
1- چگونه از Event bubbling جلوگیری کنیم؟
برخی از اوقات لازم است تا در لایه‌های تو در تو، به ازای هر لایه، کد خاصی اجرا شود. یعنی با کلیک بر روی child3 نمی‌خواهیم رویداد مربوط به parent یا حتی child2 اجرا شوند. در این حالت باید از event.stopPropagation در بدنه‌ی هندلر استفاده کنیم.

2- چگونه می‌توان تشخیص داد که بر روی کدام لایه یا المنت کلیک شده است؟
شما با استفاده از event.event.target، به شیء هدف دسترسی خواهید داشت. برای مثال اگر قصد داشته باشیم که قسمتی از کدهای ما فقط بر روی یک المنت خاص اجرا شوند، می‌توانیم به شکل زیر آنها را تفکیک کنیم:
        var elemnt = $(event.target);
        if (elemnt.attr('id') === 'parent') {
            alert('this is parnet');
        }
        else if (elemnt.attr('id') === 'child2') {
            alert('this is child2');
        }
البته نوشتن شرط برای همه‌ی المنت‌ها در یک هندلر هم باعث طولانی شدن کدها و هم تولید کد اضافه خواهد شد. خوشبختانه جی کوئری، مدیریت و ثبت رخدادها را هوشمندانه انجام می‌دهد. به جای نوشتن شرط، به راحتی کدهای مربوط به هر المنت را در یک رجیستر کننده‌ی جدا بنویسید و در نهایت جی کوئری آن‌ها را برای شما به یک هندلر منتقل خواهد کرد:
$(document).ready(function () {

    $(document).on('click','#parent', function (event) {

    });

    $(document).on('click','#child1', function (event) {

    });

    $(document).on('click','#child2', function (event) {
        event.st
    });

});

یکی دیگر از مهمترین مزایای کدنویسی به شکل فوق اینست که حتی رویدادهای مربوط به اشیایی که به صورت پویا به سند اضافه می‌شوند، اجرا خواهند شد.
در صفحه‌ی اصلی همین سایت بر روی دکمه‌ی بارگزاری بیشتر کلیک کنید. پس از اضافه شدن پست‌ها سعی کنید به یک پست امتیاز دهید. اتفاقی نخواهد افتاد. زیرا برای عناصری که بصورت پویا به صفحه اضافه شده‌اند رویدادی ثبت نشده است، که اگر از کدهای فوق استفاده شود با کمترین هزینه به هدف دلخواه خود خواهیم رسید.
پس همیشه رویدادها را تا حد امکان بر روی عنصر ریشه تعریف کنید.
دیدن لینک زیر برای اجرای یک تست و درک بهتر مطلب خالی از لطف نخواهد بود:
http://jsperf.com/jquery-body-delegate-vs-document-delegate

نظرات

  • محسن خان در ۱۳۹۴/۰۸/۲۷ ۱۴:۳۳
    روشی که در مطلب جاری توصیه شده (اتصال و مدیریت تمام رخدادگردان‌ها در سطح document)، در حالت عمومی بهتر است استفاده نشود و به همین جهت متد live را بازنشسته اعلام کردند؛ چون متد live هم تمام رخدادگردان‌ها را در سطح document مدیریت می‌کرد و کارآیی خوبی نداشت. از این جهت که زمانیکه تعداد زیادی سلکتور در سطح document قرار هست رخدادگردانی شوند (یک برنامه‌ی واقعی متشکل هست از بررسی و تعریف تعداد زیادی سلکتور)، هر رخداد انتشار یافته به سطحی بالاتر هم در اینجا باید با تمام سلکتورهای موجود مقایسه و اعتبارسنجی شود که آیا مرتبط به آن هست یا خیر. این کار، با تعداد اشیاء بالای در یک صفحه و استفاده‌ی گسترده‌ی از jQuery خیلی کند هست. برای مطالعه‌ی بیشتر
    • علی سالمیان در ۱۳۹۴/۰۸/۲۷ ۱۸:۲
      اجرای یک هندلر مستقیم هیچ برتری از لحاظ سرعت (که در توضیحات لینک مربوطه گفته شده) نسبت به هندلرهای delegate ندارد. در واقع مگر مقایسه بر فرض مثال 10000 شرط در یک رویداد چقدر زمان میبرد که بخواهیم حرف از مقایسه سرعت بزنیم؟ در حالیکه اگر 10000 هندلر مستقیم برای رویداد اشیا مختلف نوشته شود میزان زیادی حافظه مصرف شده و بار کاری پردازنده هم بالا خواهد رفت.
      برای درک بهتر موضوع یک صفحه ایجاد کنید و در آن یک جدول با 10000 سطر قرار دهید. (می توانید این سطرها را با اسکریپت بسازید) بعد در هر بار ساختن سطر جدید یک هندلر به رویداد کلیک سطر بایند کنید. سپس میزان حافظه مصرفی و زمان CPU Load را اندازه گیری کنید.
      برای مبحث سرعت هم روی یک سطر کلیک کنید و زمان اجرای هندلر را اندازه گیری کنید.
      سپس همین آزمایش را فقط با یک delegate انجام دهید و موارد بالا را اندازه گیری کنید تا ببینید آیا زمان و سرعتی که از آن صحبت می‌شود ارزش مقایسه دارد یا خیر.
      • محسن خان در ۱۳۹۴/۰۸/۲۸ ۰:۱۵
        چند نکته در اینجا حائز اهمیت هستند:
        - متد jQuery live منسوخ و حذف شده معادل هست با اتصال تمام رخدادها در سطح document یا همان (document).on('event', 'selector', function) (+). 
        - مشکلات متد live و خصوصا بحث انتشار رخدادها و بررسی هر باره‌ی انواع و اقسام آن‌ها و در نتیجه کند شدن صفحه، مهم‌تر هستند تا میزان مصرف حافظه (+).
        • علی سالمیان در ۱۳۹۴/۰۸/۲۸ ۷:۵۸
          در واقع دلیل حذف live و delegate و bind همه برای این بود که در یک متد on جمع آوری بشن. در حال حاضر on کار هر سه مورد رو انجام میده.
          اگر به تستی که گقتم دقت کنید موجه میشین که بایند کردن یک رویداد به یک هندلر مستقیم اونقدر در سرعت تاثیری نداره که بخوایم اون رو فدای مصرف حافظه کنیم.
          شما اگر بخواین در یک صفحه که کلی لینک مشابه هم داره و به ازای هر لینک که همه یک کار خاص انجام میدن یک هندلر داشته باشی به هیچ عنوان کار منطقی نیست.
          تستی رو گفتم حتما انجام بدین تا متوجه بشین تفاوت بکارگرفتن و نگرفتن این دو روش در چی هست.