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

آشنایی با توابع آسنکرون

در آخرین بخش این فصل قصد داریم با مفهومی به نام توابع آسنکرون (Async Functions) آشنا شویم. توابع آسنکرون یک ویژگی نسبتاً جدید در جاوا اسکریپت هستند که در استاندارد ECMAScript 2017 یا ES8 به این زبان اضافه شده‌اند. با استفاده از توابع آسنکرون می‌توان کار با پرامیس‌ها را تا حد زیادی ساده‌تر کرد. در بخش‌های قبل دیدیم که به طور کلی سبک برنامه‌نویسی آسنکرون نسبت به برنامه‌نویسی سنکرون کمی پیچیده‌تر است. اما با استفاده از پرامیس‌ها توانستیم این پیچیدگی را کمی کاهش دهیم. حال می‌خواهیم با استفاده از توابع آسنکرون باز هم میزان پیچیدگی برنامه‌های آسنکرون را کمتر کنیم.

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

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

نکته : توابع آسنکرون مبتنی بر پرامیس‌ها هستند. بنابراین برای درک صحیح توابع آسنکرون، باید آشنایی خوبی با پرامیس‌ها داشته باشید.

نکته : با توجه به اینکه توابع آسنکرون در استاندارد ES8 به جاوا اسکریپت اضافه شده‌اند. امکان استفاده از این ویژگی در مرورگرهای قدیمی وجود ندارد. هرچند با استفاده از کتابخانه‌های Polyfill و یا Transpiler ها می‌توان این امکان را به مرورگرهای نسبتاً قدیمی هم اضافه کرد. اما در این کتاب از این ابزارها استفاده نخواهیم کرد. بنابراین با فرض عدم استفاده از این ابزارها، مطالب این بخش فقط در مرورگرهای زیر قابل استفاده می‌باشند.

 

تعریف توابع آسنکرون

توابع آسنکرون مانند توابع معمولی تعریف می‌شوند. با این تفاوت که قبل از تعریف توابع آسنکرون باید از کلمه‌ی کلیدی async استفاده کنیم. قطعه کد زیر نحوه‌ی تعریف یک تابع آسنکرون به نام fun را نشان می‌دهد.


async function fun(){
	// body of the function
}

البته توابع آسنکرون را می‌توان به صورت بی‌نام یا به صورت Function Expression و یا به صورت Arrow Function نیز تعریف کرد. به عنوان مثال در دستورات زیر یک تابع آسنکرون به صورت Function Expression و یک تابع آسنکرون به صورت Arrow Function ایجاد شده است.


const fun1 = async function(){
	// body of the function
}

const fun2 = async () => {
	// body of the function
}

مقدار بازگشتی از یک تابع آسنکرون همیشه یک شئ از نوع Promise است. حتی اگر نوع داده‌ای که با دستور return بازگردانده می‌شود از نوع Promise نباشد، باز هم داده‌ی مورد نظر به عنوان نتیجه‌ی یک پرامیس بازگردانده می‌شود. مثلاً تابع آسنکرون زیر را در نظر بگیرید که مقدار عددی 10 را به عنوان نتیجه بازمی‌گرداند. اما در عمل تابع آسنکرون زیر یک پرامیس را بازمی‌گرداند که پس از پایان اجرا، مقدار 10 را به تابع onFulfilled ارسال می‌کند. در نتیجه برای دسترسی به مقدار 10، باید از متد then و یک تابع Callback به عنوان تابع onFulfilled استفاده شود.


async function fun(){
	return 10
}

const p = fun();
p.then((value) => console.log(value));
← 10

مشاهده می‌کنید که مقدار 10 به عنوان آرگومان به تابع onFulfilled ارسال می‌شود. سپس این مقدار در کنسول نمایش داده می‌شود. البته برای کوتاه‌سازی کدها می‌توان از دستور زیر نیز استفاده کرد. اما برای خوانایی بیشتر کدها معمولاً چنین کاری را انجام نمی‌دهیم.


p.then(console.log);

در این دستور از نام یک تابع (یا متد) موجود به عنوان تابع onFulfilled استفاده شده است. در نتیجه مقدار 10 به عنوان آرگومان به متد log از شئ console ارسال می‌شود. این متد نیز مقدار 10 را در کنسول نمایش می‌دهد.

 

کلمه‌ی کلیدی await

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

در مثال زیر تابعی به نام wait ایجاد شده است که یک پرامیس را بازمی‌گرداند. تابع wait یک عدد را نیز به عنوان ورودی دریافت می‌کند که این عدد تعداد ثانیه‌هایی را تعیین می‌کند که تابع executor باید منتظر بماند، سپس تابع resolve را فراخوانی کند تا اجرای پرامیس با موفقیت به پایان برسد. همچنین عدد ثانیه‌ی زمان جاری سیستم به عنوان آرگومان به تابع resolved ارسال می‌شود. که در نهایت این عدد به تابع onFulfilled نیز ارسال خواهد شد.


function wait(n){
	return new Promise(function(resolve , reject){		
		setTimeout(() => {
			let seconds = new Date().getSeconds();
			return resolve(seconds);
		} , n * 1000);
	});
}

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


wait(3)
	.then((value) => {
		console.log(value);
		return wait(4);
	})
	.then((value) => {
		console.log(value);
		return wait(5);
	})
	.then((value) => {
		console.log(value);
	});

مشاهده می‌کنید که برای اجرای متوالی پرامیس‌ها، متد then به صورت زنجیره‌ای استفاده شده است. این مثال را می‌توانید اینجا اجرا کنید. با اجرای این مثال خواهید دید که ۳ عدد مختلف که بین 0 تا 59 هستند، با تاخیرهای زمانی مشخص شده در کنسول نمایش داده می‌شوند. حال می‌خواهیم با استفاده از یک تابع آسنکرون همین کار را به شکل ساده‌تری انجام دهیم. برای این منظور تابع زیر را تعریف می‌کنیم.


async function myAsync(){
	let value = await wait(3);
	console.log(value);
	value = await wait(4);
	console.log(value);
	value = await wait(5);
	console.log(value);
}

مشاهده می‌کنید که در دستورات فوق از کلمه‌ی کلیدی await استفاده شده است. استفاده از کلمه‌ی کلیدی await می‌تواند تا حد زیادی نوشتن برنامه‌های آسنکرون را ساده‌تر کند. در واقع در این حالت برنامه‌های آسنکرون به سبک برنامه‌های سنکرون نوشته می‌شوند و همانطور که در این مثال می‌بینید، از توابع Callback و یا متد then در این دستورات استفاده نشده است. اما کلمه‌ی کلیدی await دقیقاً چه کاربردی دارد و به چه شکل عمل می‌کند؟

اگر در زمان فراخوانی تابعی که خروجی آن یک پرامیس است، قبل از نام تابع از کلمه‌ی کلیدی await استفاده کنیم. مفسر جاوا اسکریپت دستورات بعدی را اجرا نمی‌کند تا زمانی که پرامیس بازگردانده شده از آن تابع کار خود را به پایان برساند. یعنی در این مثال پس از فراخوانی تابع wait در خط 2، مفسر 3 ثانیه منتظر می‌ماند تا اجرای پرامیس به پایان برسد. سپس مقدار بازگردانده شده از پرامیس در متغیر value ذخیره می‌شود. در خط 3 نیز مقدار متغیر value در کنسول نمایش داده می‌شود.

در خطوط بعدی نیز دو بار تابع wait فراخوانی می‌شود و نتیجه‌ی این فراخوانی‌ها در کنسول نمایش داده می‌شود. و همانطور که مشاهده می‌کنید سبک دستورات موجود در تابع myAsync کاملاً مشابه برنامه‌های سنکرون است و خوانایی بالایی دارد. پس با استفاده از کلمه‌ی کلیدی await می‌توان مفسر را تا زمانی که اجرای یک پرامیس به پایان برسد در حالت انتظار قرار داد. که با استفاده از این ویژگی می‌توان برنامه‌های آسنکرون را به سبک برنامه‌های سنکرون پیاده‌سازی کرد. البته برای اجرای این مثال لازم است تا تابع myAsync را فراخوانی کنیم. همچنین برای کوتاه‌سازی بیشتر کدها می‌توان از توابع IIFE به شکل زیر استفاده کرد.


(async function(){
	let value = await wait(3);
	console.log(value);
	value = await wait(4);
	console.log(value);
	value = await wait(5);
	console.log(value);
})();

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

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


console.log('Before');
(async function(){
	let value = await wait(3);
	console.log(value);
	value = await wait(4);
	console.log(value);
	value = await wait(5);
	console.log(value);
})();
console.log('After');

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

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

حال بازمی‌گردیم به مثالی که در بخش‌های قبلی چند بار به روش‌های مختلف آن را پیاده‌سازی کردیم. مثالی که ابتدا آن را با استفاده از شئ XMLHttpRequest و بدون استفاده از پرامیس‌ها پیاده‌سازی کردیم که منجر به تشکیل جهنم Callback شد. سپس با استفاده از پرامیس‌ها مشکل جهنم Callback را برطرف کردیم. و در نهایت با استفاده از Fetch API و عدم استفاده از شئ XMLHttpRequest تا حد زیادی حجم کدها را کاهش داده و خوانایی کدها را افزایش دادیم.

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


(async function(){
	let response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
	let obj = await response.json();
	console.log(obj.id);
	response = await fetch('https://jsonplaceholder.typicode.com/todos/2')
	obj = await response.json();
	console.log(obj.id);
	response = await fetch('https://jsonplaceholder.typicode.com/todos/3')
	obj = await response.json();
	console.log(obj.id);
	response = await fetch('https://jsonplaceholder.typicode.com/todos/4')
	obj = await response.json();
	console.log(obj.id);
})();

این مثال را می‌توانید اینجا اجرا کنید. با اجرای این مثال مشاهده خواهید کرد که نتیجه‌ی آن با پیاده‌سازی‌های قبلی کاملاً یکسان است. توجه کنید که برای دریافت نتیجه‌ی متد json نیز از کلمه‌ی کلیدی await استفاده شده است. زیرا این متد نیز یک پرامیس را بازمی‌گرداند.

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

 

مدیریت خطاها در توابع آسنکرون

برای مدیریت خطاها در توابع آسنکرون دو روش وجود دارد. روش اول این است که در بدنه‌ی تابع از بلاک try..catch استفاده کنیم. در این صورت باید تمام دستورات اصلی تابع در بلاک try قرار گیرند. و دستورات مربوط به مدیریت خطا باید در بلاک catch قرار داده شوند. قطعه کد زیر این حالت را نشان می‌دهد.


async function fun(){
	try{
		// دستورات اصلی تابع
	}catch(error){
		// دستورات مدیریت خطا
	}
}

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


async function fun(){
	// دستورات تابع
}

p = fun();
p.then(onFulfilled).catch(onRejected);

در این روش در صورت بروز خطا در دستورات تابع آسنکرون، تابع onRejected فراخوانی خواهد شد. البته استفاده از متد then و تابع onFulfilled در این شرایط اختیاری است. یعنی اگر فقط مدیریت خطا مورد نظر باشد، نیازی به استفاده از متد then نیست. همچنین توجه کنید که اگر به صورت همزمان از هر دو روش استفاده کنید. در صورت بروز خطا، بلاک try..catch عمل خواهد کرد و از متد catch صرف‌نظر خواهد شد.

 

نکته‌ی مهم : در این فصل مثال‌های زیادی از کاربرد پرامیس‌ها در ارسال درخواست‌های HTTP (یا Ajax) ارائه شد. اما باید توجه داشته باشید که برنامه‌نویسی آسنکرون محدود به ارسال درخواست‌های HTTP نیست. یعنی پرامیس‌ها می‌توانند در سایر موضوعاتی که مرتبط با برنامه‌نویسی آسنکرون هستند نیز مورد استفاده قرار گیرند. به عنوان مثال یکی دیگر از کاربردهای پرامیس‌ها، زمانبندی کردن انیمیشن‌ها است. یعنی برای اعمال کردن چند انیمیشن به صورت متوالی به یکی از عناصر صفحه، می‌توان از پرامیس‌ها به صورت زنجیره‌ای استفاده کرد. نمونه‌ای از این کاربرد را می‌توانید اینجا ببینید. همچنین با استفاده از توابع آسنکرون می‌توان کار با پرامیس‌ها را در چنین مواردی نیز ساده‌تر کرد.