بازگشت به دوره

آشنایی با جهنم Callback

یکی از مشکلاتی که معمولاً برنامه‌نویسان جاوا اسکریپت در برنامه‌نویسی آسنکرون (و همچنین برنامه‌نویسی رویداد محور) با آن روبرو می‌شوند، مسئله‌ی "جهنم Callback" یا "Callback Hell" است. در این بخش با چند مثال ساده با این مفهوم آشنا می‌شوید. سپس در بخش‌های بعدی به بررسی روش‌های جلوگیری از به وجود آمدن جهنم Callback می‌پردازیم.

پیش از این بارها و بارها از توابع Callback برای مقاصد مختلف استفاده کرده‌ایم. یکی از مهمترین کاربردهای توابع Callback، تعریف توابعی به عنوان Event Handler است. این Event Handler ها می‌توانند مربوط به رویدادهای Ajax یا سایر انواع رویدادها باشند. زمانی که تعدادی تابع Callback به صورت تو در تو تعریف می‌شوند، اصطلاحاً جهنم Callback به وجود می‌آید. دلیل استفاده از این اصطلاح برای این وضعیت، پایین آمدن خوانایی کدها می‌باشد. یعنی در چنین شرایطی معمولاً درک کردن کدها توسط انسان کار ساده‌ای نیست.

از توابع Callback تو در تو معمولاً زمانی استفاده می‌شود که اجرا شدن یک Event Handler، وابسته به اجرا شدن Event Handler دیگری باشد. به عبارت دیگر اجرای توابع Callback دارای یک ترتیب خاص است و رویدادهای مرتبط با آنها با یک ترتیب مشخص رخ می‌دهند.

به عنوان مثال به دستورات زیر توجه کنید.


const xhr1 = new XMLHttpRequest();
xhr1.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');
xhr1.addEventListener('load', function () {
	if ((xhr1.status >= 200 && xhr1.status < 300) || xhr1.status == 304) {
		console.log(JSON.parse(xhr1.responseText).id);
	}
});
xhr1.send();

const xhr2 = new XMLHttpRequest();
xhr2.open('GET', 'https://jsonplaceholder.typicode.com/todos/2');
xhr2.addEventListener('load', function () {
	if ((xhr2.status >= 200 && xhr2.status < 300) || xhr2.status == 304) {
		console.log(JSON.parse(xhr2.responseText).id);
	}
});
xhr2.send();

const xhr3 = new XMLHttpRequest();
xhr3.open('GET', 'https://jsonplaceholder.typicode.com/todos/3');
xhr3.addEventListener('load', function () {
	if ((xhr3.status >= 200 && xhr3.status < 300) || xhr3.status == 304) {
		console.log(JSON.parse(xhr3.responseText).id);
	}
});
xhr3.send();

const xhr4 = new XMLHttpRequest();
xhr4.open('GET', 'https://jsonplaceholder.typicode.com/todos/4');
xhr4.addEventListener('load', function () {
	if ((xhr4.status >= 200 && xhr4.status < 300) || xhr4.status == 304) {
		console.log(JSON.parse(xhr4.responseText).id);
	}
});
xhr4.send();

در کدهای فوق ۴ درخواست Ajax به صورت متوالی به ۴ آدرس متفاوت ارسال می‌شود. پاسخ هر یک از این درخواست‌ها یک شئ JSON است. پس از دریافت پاسخ هر یک از درخواست‌ها، مقدار خاصیت id از شئ دریافت شده در کنسول نمایش داده می‌شود.

این مثال را می‌توانید اینجا اجرا کنید. با اجرای این مثال مشاهده خواهید کرد که ترتیب دریافت پاسخ‌ها، با ترتیب ارسال درخواست‌ها یکسان نیست. مثلاً ممکن است ابتدا پاسخ درخواست دوم، سپس پاسخ درخواست اول و بعد از آن پاسخ درخواست چهارم و در نهایت پاسخ درخواست سوم دریافت شود. با چند بار اجرای این مثال مشاهده خواهید کرد که ترتیب دریافت پاسخ‌ها در اجراهای مختلف یکسان نیست. این در حالی است که در تمام اجراها ترتیب ارسال درخواست‌ها یکسان است. زیرا دستورات مربوط به ارسال درخواست‌ها به صورت سنکرون اجرا می‌شوند.

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

حال فرض کنید که شرایط به گونه‌ای است که پاسخ این درخواست‌ها باید با همان ترتیب ارسال درخواست‌ها دریافت شوند. در این صورت تا زمانی که پاسخ درخواست اول دریافت نشده باشد، نباید درخواست دوم ارسال شود. در نتیجه لازم است از توابع Callback به صورت تو در تو استفاده کنیم. یعنی در Event Handler مربوط به هر درخواست Ajax، درخواست بعدی را ارسال کنیم. دستورات زیر نسخه‌ی اصلاح شده‌ی دستورات فوق را نشان می‌دهند.


const xhr1 = new XMLHttpRequest();
xhr1.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');
xhr1.addEventListener('load', function () {
	if ((xhr1.status >= 200 && xhr1.status < 300) || xhr1.status == 304) {
		console.log(JSON.parse(xhr1.responseText).id);
		const xhr2 = new XMLHttpRequest();
		xhr2.open('GET', 'https://jsonplaceholder.typicode.com/todos/2');
		xhr2.addEventListener('load', function () {
			if ((xhr2.status >= 200 && xhr2.status < 300) || xhr2.status == 304) {
				console.log(JSON.parse(xhr2.responseText).id);
				const xhr3 = new XMLHttpRequest();
				xhr3.open('GET', 'https://jsonplaceholder.typicode.com/todos/3');
				xhr3.addEventListener('load', function () {
					if ((xhr3.status >= 200 && xhr3.status < 300) || xhr3.status == 304) {
						console.log(JSON.parse(xhr3.responseText).id);
						const xhr4 = new XMLHttpRequest();
						xhr4.open('GET', 'https://jsonplaceholder.typicode.com/todos/4');
						xhr4.addEventListener('load', function () {
							if ((xhr4.status >= 200 && xhr4.status < 300) || xhr4.status == 304) {
								console.log(JSON.parse(xhr4.responseText).id);
							}
						});
						xhr4.send();
					}
				});
				xhr3.send();
			}
		});
		xhr2.send();
	}
});
xhr1.send();

این مثال را می‌توانید اینجا اجرا کنید. با اجرای این مثال مشاهده خواهید کرد که ترتیب دریافت پاسخ‌ها دقیقاً به صورت 1، 2، 3 و 4 خواهد بود. به هر تعداد که این مثال را اجرا کنید، همین نتیجه را دریافت خواهید کرد.

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

لازم به ذکر است که چنین شرایطی در جاوا اسکریپت به وفور رخ می‌دهد. یعنی استفاده از توابع Callback به صورت تو در تو در جاوا اسکریپت از گذشته بسیار مرسوم بوده است. این یکی از معضلاتی است که برنامه‌نویسان جاوا اسکریپت از گذشته با آن مواجه بوده‌اند. به خصوص در نوشتن برنامه‌های بزرگ و پیچیده استفاده از توابع Callback به صورت تو در تو اجتناب‌ناپذیر بوده است. این همان شرایطی است که به "جهنم Callback" مشهور است و مشکلات زیادی را برای برنامه‌نویسان ایجاد می‌کند.

البته در نسخه‌ی ششم از استاندارد ECMAScript یا ES6، تمهیداتی برای حل معضل "جهنم Callback" در نظر گرفته شده است. در ES6 با استفاده از Promise ها می‌توان تا حد زیادی از به وجود آمدن جهنم Callback جلوگیری کرد. همچنین در ES8 نیز امکانات دیگری به ECMAScript اضافه شده است که با استفاده از این امکانات نیز می‌توان وضعیت برنامه‌نویسی آسنکرون را بهبود بخشید. در ادامه‌ی این فصل به بررسی این ویژگی‌های جدید در ECMAScript خواهیم پرداخت.