Callback: قلب برنامه‌نویسی رویدادمحور و ناهمزمان

مقدمه

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

Callback دقیقاً چیست؟

Callback تابعی است که به‌عنوان آرگومان به تابع دیگری پاس داده می‌شود، با این هدف که آن تابع دیگر، در زمان یا شرایط مناسب، آن را فراخوانی کند. نام «Callback» به همین معناست: تابعی که قرار است بعداً «صدا زده شود» (Called Back).

function processUser(name, callback) {
  let processedName = name.trim().toUpperCase();
  callback(processedName);
}

processUser("  علی  ", function (result) {
  console.log("نتیجه‌ی پردازش: " + result);
});

در این مثال، تابع بی‌نامی که به‌عنوان آرگومان دوم پاس داده شده، یک Callback است. تابع processUser پس از پردازش نام، این Callback را با مقدار نهایی فراخوانی می‌کند.

دو نوع اصلی Callback: همزمان و ناهمزمان

نکته‌ی مهمی که باید از همین ابتدا روشن شود: Callback بودن یک تابع، به‌تنهایی چیزی درباره‌ی زمان اجرای آن نمی‌گوید. Callback ها به دو دسته‌ی کلی تقسیم می‌شوند:

۱. Callback همزمان (Synchronous Callback)

Callback ای که بلافاصله و در همان لحظه‌ی اجرای تابع اصلی فراخوانی می‌شود؛ مثل تمام مثال‌های map، filter و forEach که در فصل آرایه دیدیم:

Callback همزمان: اجرا بلافاصله انجام می‌شود
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل کد

خروجی این کد دقیقاً به همان ترتیبی چاپ می‌شود که نوشته شده: "قبل از map"، سپس سه بار "در حال پردازش..."، و در نهایت "بعد از map". این نشان می‌دهد که Callback داخل map کاملاً همزمان و بدون هیچ تأخیری، در همان لحظه‌ی فراخوانی map اجرا می‌شود؛ دقیقاً طبق مدل تک‌رشته‌ای و Call Stack که در فصل Execution Model بررسی کردیم.

۲. Callback ناهمزمان (Asynchronous Callback)

Callback ای که اجرای آن به تعویق می‌افتد و در آینده -پس از اتمام یک عملیات زمان‌بر مثل تایمر یا دریافت پاسخ از سرور- فراخوانی می‌شود:

Callback ناهمزمان: اجرا به تعویق می‌افتد
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل رفتار جاوااسکریپت

برخلاف مثال قبلی، ترتیب خروجی این کد این‌گونه است: "قبل از setTimeout"، سپس بلافاصله "بعد از setTimeout"، و تنها پس از گذشت یک ثانیه، پیام تأخیری چاپ می‌شود. جاوااسکریپت منتظر تایمر نمی‌ماند؛ خط بعدی کد ("بعد از setTimeout") بلافاصله اجرا می‌شود و Callback داخل setTimeout فقط زمانی که تایمر تمام شد، به نوبت اجرا در می‌آید. دلیل دقیق و مکانیزم پشت این رفتار (شامل مفاهیمی مثل Web APIs، Task Queue و Event Loop) موضوع کامل فصل Asynchronous JavaScript خواهد بود؛ در این‌جا فقط لازم است تفاوت رفتاری این دو نوع Callback را به‌خوبی تشخیص دهید.

جدول مقایسه‌ی Callback همزمان و ناهمزمان

ویژگیCallback همزمانCallback ناهمزمان
زمان اجرابلافاصله، در همان لحظه‌ی فراخوانیدر آینده، پس از اتمام یک عملیات دیگر
مثالmap، filter، forEachsetTimeout، درخواست شبکه
تأثیر بر ترتیب اجرای کدهیچ تغییری در ترتیب معمول ایجاد نمی‌کندکد بعد از آن، زودتر از Callback اجرا می‌شود

Callback به‌عنوان الگوی مدیریت خطا

یک الگوی رایج و قدیمی‌تر در جاوااسکریپت، استفاده از Callback برای گزارش هم نتیجه و هم خطای احتمالی یک عملیات است؛ معمولاً با این قرارداد که آرگومان اول Callback، خطا (در صورت وجود) و آرگومان دوم، نتیجه‌ی موفق است:

function fetchUserData(userId, callback) {
  if (userId <= 0) {
    callback("شناسه‌ی کاربر نامعتبر است", null);
    return;
  }
  callback(null, { id: userId, name: "علی" });
}

fetchUserData(5, function (error, data) {
  if (error) {
    console.log("خطا: " + error);
    return;
  }
  console.log("داده دریافت شد: " + data.name);
});

این الگو با عنوان Error-First Callback شناخته می‌شود و پیش از رواج Promise و async/await (که در فصل Asynchronous JavaScript بررسی خواهیم کرد)، رایج‌ترین روش مدیریت عملیات ناهمزمان در جاوااسکریپت بود.

مشکل معروف: Callback Hell

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

getUser(1, function (user) {
  getPosts(user.id, function (posts) {
    getComments(posts[0].id, function (comments) {
      console.log(comments);
      // هرچه عملیات بیشتر شود، تو در تو شدن هم بیشتر می‌شود
    });
  });
});

این وضعیت را در جامعه‌ی برنامه‌نویسی جاوااسکریپت، Callback Hell (یا Pyramid of Doom) می‌نامند. راه‌حل مدرن این مشکل، استفاده از Promise و async/await است که هر دو را با جزئیات کامل در فصل Asynchronous JavaScript بررسی خواهیم کرد؛ در همین‌جا کافی است بدانید که این مشکل، یکی از دلایل اصلی معرفی Promise به زبان جاوااسکریپت بوده است.

ارتباط با سایر مفاهیم

Callback مستقیماً بر پایه‌ی مفهوم Higher-Order Function در درس قبل بنا شده است. تفاوت Callback همزمان و ناهمزمان، پیش‌زمینه‌ی ضروری برای فهم Event Loop، Task Queue و Microtask Queue در فصل Asynchronous JavaScript است. همچنین، در فصل Events مربوط به Browser و DOM، خواهید دید که تقریباً تمام Event Handler ها (مثل واکنش به کلیک کاربر) در واقع نوعی Callback ناهمزمان هستند.

جمع‌بندی

Callback تابعی است که به‌عنوان آرگومان به تابع دیگری پاس داده می‌شود تا در زمان مناسب فراخوانی شود. Callback ها به دو دسته‌ی همزمان (که بلافاصله اجرا می‌شوند، مثل map) و ناهمزمان (که اجرایشان به تعویق می‌افتد، مثل setTimeout) تقسیم می‌شوند. الگوی Error-First Callback روشی رایج برای مدیریت خطا در Callback هاست، و تو در تو شدن بیش‌ازحد Callback های ناهمزمان به مشکلی به نام Callback Hell منجر می‌شود که راه‌حل مدرن آن، Promise و async/await است.