آشنایی با مفهوم Callback در زبان برنامه‌نویسی جاوااسکریپت


در پاسخ به این پرسش که «کال‌بَک چیست و چه کاربردهایی دارا است؟» می‌توان هم پاسخی ساده و مختصر و هم پاسخی کامل و البته کمی پیچیده ارائه کرد که برای بیان یک تعریف ساده از این مفهوم باید گفت که Callback روشی است که با به‌کارگیریِ آن دولوپرها می‌توانند اجرای یک فانکشن را ملزم به اتمام اجرای یک فانکشن دیگر کنند بدین معنی که فانکشن مذکور تنها در صورتی اجرا شود که اجرای فانکشن دیگر به پایان رسیده باشد و از همین روی این روش Call back به معنی «فراخوانی مجدد» نیز نامیده می‌شود.

اولین کسی باشید که به این سؤال پاسخ می‌دهید

آشنایی با نحوۀ اجرای فانکشن‌ها در زبان برنامه‌نویسی جاوااسکریپت
اما به منظور ارائۀ تعریف کاملی برای مفهوم Callback در زبان برنامه‌نویسی جاوااسکریپت، باید این نکته را در نظر داشته باشیم که در جاوااسکریپت دولوپرها می‌توانند هر یک از فانکشن‌های مد نظر خود را به عنوان یک آرگومان ورودی به فانکشنی دیگر پاس دهند به طوری که اجرای فانکشن مذکور وابسته به اتمام اجرای فانکشن دیگر باشد. در واقع، فانکشنی که فانکشن دیگری را به آرگومان ورودی دریافت می‌کند اصطلاحاً Higher-order Function نامیده می‌شود و فانکشنی که به عنوان آرگومان ورودی پاس داده می‌شود به اصطلاح Callback Function نامیده می‌شود.

جاوااسکریپت یک زبان اصطلاحاً Event-Driven (مبتنی بر رویداد) است بدین معنا که کلیۀ تَسک‌ها در این زبان توسط یک Event Loop هندل می‌شوند که اگر اجرای تَسکی زمان‌بَر باشد، جاوااسکریپت به سراغ اجرای تَسک بعدی می‌رود بدین صورت که هر یک از تَسک‌ها به جای انتظار برای دریافت ریسپانس از اجرای تَسک زمان‌بَر قبلی، وارد صف Callback می‌شوند و تَسک زمان‌بَر نیز پس از اتمام اجرا وارد این صف شده و در ادامه Event Loop که مسئول هندل کردن تَسک‌ها است، بر اساس قانون First In First Out یا به اختصار FIFO یک‌به‌یک تَسک‌های مذکور را به ترتیب ورودی از صف خارج کرده و اجرا می‌کند.

در Node.js نیز این قابلیت در اختیار دولوپرها قرار می‌گیرد تا بتوانند تَسک‌های مد نظر خود را به صورت Asynchronous اجرا کنند که در آن هیچ یک از تَسک‌ها بلاک نشده و به صورت هم‌زمان و موازی اجرا می‌شوند که برای آشنایی بیشتر با این محیط جاوااسکریپتی سمت سرور و نحوۀ کار Event Loop در آن می‌توانید به مقالۀ Node.js چیست؟ مراجعه نمایید. در واقع، زبان جاوااسکریپت امکانی را برای دولوپرها فراهم می‌آورد تا بتوانند اپلیکیشنی را توسعه دهند که در آن تمامی فرآیندهای I/O (ورودی/خروجی) به صورت اصطلاحاً Non-Blocking اجرا شوند که برای آشنایی بیشتر با این مفهوم نیز توصیه می‌کنیم به مقالۀ آشنایی با مفاهیم Blocking و Non-Blocking در Node.js مراجعه نمایید.

برای درک بهتر این موضوع، مثال زیر را مورد بررسی قرار می‌دهیم تا ببینیم اجرای تَسک‌ها به صورت هم‌زمان در زبان برنامه‌نویسی جاوااسکریپت چگونه انجام می‌شود:

function first() {
    console.log(1);
}
function second() {
    console.log(2);
}
first();
second();

همان‌طور که در کد فوق می‌بینید در ابتدا فانکشنی تحت عنوان ()first تعریف کرده‌ایم که این وظیفه را دارا است تا عدد 1 را در کنسول چاپ کند و در ادامه فانکشن دیگری تحت عنوان ()second تعریف کرده‌ایم که وظیفۀ چاپ عدد 2 را دارا است و در ادامه نیز هر دو فانکشن را فراخوانی کرده‌ایم و همان‌طور که انتظار می‌رود، فانکشن اول در ابتدا اجرا شده و در ادامه فانکشن دوم اجرا می‌شود که در نتیجه مقادیر عددیِ زیر به ترتیب در خروجی نمایش داده می‌شوند:

1
2

اما اگر چنانچه فانکشن ()first حاوی کُدی باشد که لزوماً بلافاصله اجرا نشده و بازگشت ریسپانس حاصل از اجرای آن نیاز به اندکی زمان داشته باشد به طوری که فرض کنیم این وظیفه را دارا است که یک ریکوئست به API یک سرویس خاص ارسال کرده و منتظر بازگشت ریسپانس از آن بماند، اوضاع کاملاً‌ تغییر خواهد کرد.

در واقع، برای شبیه‌سازی چنین شرایطی از ()setTimeout استفاده می‌کنیم که یک فانکشن جاوااسکریپتی است که دو آرگومان ورودی می‌گیرد که اولی فانکشن مد نظر و دومی مدت زمان تأخیر مورد نیاز است و این امکان را فراهم می‌سازد تا فانکشنی که به عنوان آرگومان ورودی به آن داده می‌شود، پس از گذشت مدت زمانی مشخص فراخوانی و اجرا شود.

در همین راستا، در مثال زیر قصد داریم تا فانکشن ()first را با 500 میلی‌ثانیه تأخیر فراخوانی و اجرا کنیم که مثلاً شرایط ارسال یک ریکوئست به ای‌پی‌آی یک سرویس خاص و انتظار برای دریافت ریسپانس از آن را شبیه‌سازی کنیم که برای این منظور مثال قبل را به صورت زیر تغییر می‌دهیم:

function first() {
    setTimeout(function() {
        console.log(1);
    }, 500);
}
function second() {
    console.log(2);
}
first();
second();

در تفسیر کُد فوق باید بگوییم که فانکشنی تحت عنوان ()first تعریف کرده‌ایم و در آن فانکشن ()setTimeout را پیاده‌سازی کرده‌ایم بدین صورت که فانکشنی بی‌نام به صورت ()function را در قالب یک Callback Function به عنوان آرگومان ورودی به ()setTimeout می‌دهیم که در آن گفته‌ایم در صورت فراخوانی، عدد 1 را در خروجی چاپ کند و همچنین مقدار عددی 500 را به عنوان آرگومان ورودیِ دوم در نظر گرفته و گفته‌ایم که این فانکشن بدون نام را پس از 500 میلی‌ثانیه فراخوانی کرده و اجرا کند که در نهایت عدد 1 با 500 میلی‌ثانیه تأخیر در خروجی چاپ خواهد شد.

بر اساس توضیحاتی که در ابتدا به آن‌ها اشاره کردیم، جاوااسکریپت یک زبان مبتنی بر رویداد است و هیچ یک از تَسک‌ها در آن بلاک نمی‌شوند و به صورت موازی اجرا می‌شوند که از همین روی، در این مثال سیستم تحت هیچ عنوان 500 میلی‌ثانیه سایر تَسک‌ها را بلاک نمی‌کند تا منتظر اتمام اجرای فانکشن ()first بماند بلکه این فانکشن را اجرا کرده و در این فاصله به سراغ ادامۀ کُد رفته و فانکشن ()second را اجرا می‌کند و همان‌طور که مشاهده می‌کنید، در ابتدا فانکشن ()first فراخوانی شده و در فاصلۀ زمانی که این فانکشن تکمیل می‌شود، فانکشن ()second فراخوانی و بلافاصله اجرا می‌شود که از همین روی عدد 2 در ابتدا چاپ شده و در ادامه پس از مدت زمان تعیین‌شده (۵۰۰ میلی‌ثانیه) عدد 1 در خروجی چاپ می‌شود:

2
1

در واقع، این موضوع بدین معنا نیست که زبان برنامه‌نویسی جاوااسکریپت فانکشن‌های فوق را به همان ترتیب تعریف‌شده اجرا نمی‌کند بلکه حقیقت این است که منتظر دریافت ریسپانس حاصل از اجرای فانکشن ()first قبل از اجرای فانکشن ()second نمی‌ماند به طوری که فانکشن ()first را فراخوانی کرده و هم‌زمان به سراغ فراخوانی فانکشن ()second می‌رود و همین مسئله منجر بدین خواهد شد تا عدد 2 بلافاصله در خروجی چاپ شده و فانکشن ()first نیز با 500 میلی‌ثانیه تأخیر اجرا شده و عدد 1 را در خروجی نمایش دهد.

چرا از روش Callback در فراخوانی فانکشن‌ها استفاده می‌کنیم؟
در پاسخ به این پرسش باید گفت که ممکن است اجرای برخی فانکشن‌ها زمان‌بَر بوده و در صورت فراخوانی به همان ترتیب فراخوانی‌شده اجرا نشوند و بنابراین با به‌کارگیریِ روش کال‌بَک، سیستم را ملزم به رعایت این مسئله خواهیم کرد که تا زمانی که اجرای فانکشن مد نظر تمام نشده، مفسر جاوااسکریپت نتواند تَسک بعدی مد نظر ما را اجرا کند.

اما در عین حال گزارۀ فوق بدین معنا نیست که سیستم تمامی فانکشن‌ها را بلاک کرده و منتظر می‌ماند تا اجرای فانکشن مد نظر به اِتمام رسیده و در ادامه فانکشن کال‌بَک را اجرا کند بلکه با به‌کارگیریِ این روش، اجرای فانکشن کال‌بَک را ملزم به اِتمام اجرای فانکشن مد نظر می‌کنیم و این در حالی است که سیستم در صورت مشاهدۀ زمان‌بَر بودن فانکشن مذکور به سراغ اجرای تَسک‌های دیگری غیر از فانکشن کال‌بَک می‌رود و اجرای آن را به زمانی موکول می‌کند که اجرای فانکشن زمان‌بَر مذکور به اِتمام رسیده باشد که در ادامه با ذکر یک مثال نحوۀ توسعۀ اپلیکیشن با به‌کارگیری روش Callback را تشریح می‌کنیم.

برای اجرای کُدها در محیط Chrome Developer لازم است تا محیط کنسول را باز کرده و در ادامه دستورات مربوط به فانکشن زیر را در آن تایپ می‌کنیم:

function doHomework(subject) {
    alert('Starting my ' + subject + ' homework.');
}
doHomework('math');

در کد فوق فانکشنی تحت عنوان ()doHomework را تعریف کرده‌ایم و متغیری به نام subject را به عنوان آرگومان ورودی به آن می‌دهیم که قرار است تا بدین وسیله نام درس مد نظر را به عنوان پارامتر ورودی گرفته و در ادامه با استرینگ «.Starting my homework» کانکت کرده و در قالب یک پنجرۀ Alert در مرورگر نمایش دهد که برای این منظور، در ادامه فانکشن مذکور را در کنسول تایپ کرده و نام درس «math» را به عنوان پارامتر ورودی به آن داده‌ایم که در نتیجۀ این فراخوانی، استرینگ «.Starting my math homework» در قالب یک پیغام آلِرت نمایش داده خواهد شد.

حال قصد داریم تا مثال فوق را با به‌کارگیریِ روش Callback پیاده‌سازی کنیم به طوری یک تَسک خاص صرفاً پس از اِتمام اجرای فانکشن ()doHomework انجام شود که برای این منظور یک آرگومان ورودی دیگر تحت عنوان myCallback را برای این فانکشن در نظر می‌گیریم که قرار است تا بدین طریق فانکشن کال‌بَک مذکور پس از اتمام اجرای تَسک مربوط به فانکشن ()doHomework فراخوانی شده و اجرا گردد:

function doHomework(subject, myCallback) {
    alert('Starting my ' + subject + ' homework.');
    myCallback();
}
doHomework('math', function() {
    alert('Finished my homework');
});

همان‌طور که می‌بینید، فانکشن ()doHomework دارای دو آرگومان ورودی است که آرگومان اول به منظور دریافت نام درس و آرگومان دوم را برای پیاده‌سازی فانکشن کال‌بَک تعریف کرده‌ایم و در ادامه گفته‌ایم در صورت فراخوانی این فانکشن، پارامتر ورودی با استرینگ «.Starting my homework» کانکت شده و به صورت یک پیغام آلِرت نمایش داده شود و در خط بعد هم گفته‌ایم پس از اتمام اجرای تَسک فوق‌الذکر، پارامتر ورودی دوم که در نقش یک فانکشن کال‌بَک می‌باشد، فراخوانی و اجرا شود که اگر چنانچه اجرای تَسک فوق زمان‌بَر بود، جاوااسکریپت به سراغ اجرای سایر تَسک‌ها رفته و اجرای فانکشن کال‌بَک را به زمانی موکول می‌کند که اجرای تَسک زمان‌بَر به پایان رسیده باشد.

جهت تست، فانکشن ()doHomework را با دو پارامتر ورودی فراخوانی کرده‌ایم که در ابتدا نام درس «math» با استرینگ مذکور کانکت شده و در قالب پیغام آلِرت نمایش داده می‌شود و پس از اِتمام اجرای این تَسک، فانکشن کال‌بَک به صورت بی‌نام و به شکل ()function فراخوانی و اجرا می‌شود که در آن گفته‌ایم استرینگ «.Finished my homework» پس از اتمام نمایش استرینگ «.Starting my math homework» به صورت پیغام آلِرت نمایش داده شود.

فانکشن‌های کال‌بَک را به شکل دیگری نیز می‌توان پیاده‌سازی کرد که از همین روی در ادامه روش دیگری را به منظور پیاده‌سازی فانکشن کال‌بَک مربوط به مثال قبل مورد بررسی قرار می‌دهیم:

function doHomework(subject, myCallback) {
    alert('Starting my ' + subject + ' homework.');
    myCallback();
}
function alertFinished() {
    alert('Finished my homework');
}
doHomework('math', alertFinished);

همان‌طور مشاهده می‌کنید پیاده‌سازی این روش تا انتهای خط سوم مشابه روش قبل است بدین صورت که دو آرگومان ورودی برای دریافت پارامتر ورودیِ نام درس و پیاده‌سازی فانکشن کال‌بک در نظر گرفته‌ایم و در ادامه گفته‌ایم در صورت فراخوانی فانکشن ()doHomework نام درس را با استرینگ مربوطه کانکت کرده و در خروجی نمایش دهد و پس از اتمام اجرای این تَسک فانکشن کال‌بَک myCallback فراخوانی و اجرا شود.

اما در ادامه فانکشن دیگری تحت عنوان ()alertFinished تعریف کرده‌ایم و گفته‌ایم در صورت فراخوانی این فانکشن، استرینگ «.Finished my homework» به صورت پیغام آلِرت در خروجی نمایش داده شود به طوری که وقتی فانکشن ()alertFinished را به عنوان پارامتر ورودیِ دوم و در قالب یک فانکشن کال‌بک به فانکشن ()doHomework دادیم، پس از اتمام نمایش آلِرت اول فراخوانی شده و استرینگ مربوطه را در خروجی نمایش می‌دهد.

همان‌طور که می‌بینید نتیجۀ حاصل از هر دو روش یکسان بوده اما این در حالی است که در نحوۀ پیاده‌سازی فانکشن کال‌بَک متفاوت هستند به طوری که در روش دوم فانکشن ()alertFinished را به عنوان آرگومان ورودیِ دوم به فانکشن ()doHomework داده‌ایم اما در روش اول فانکشن کال‌بَک داخل فانکشن اصلی پیاده‌سازی شده است.

منبع


اکرم امراه‌نژاد