##### مفهوم ناهمزمان (Asynchronous) در جاوا اسکریپت به زبان ساده
فرض کنید به یک کافه رفتهاید. شما سفارش خود را میدهید و منتظر میمانید تا آماده شود. در این مدت، میتوانید به اطراف نگاه کنید، با دوستتان صحبت کنید یا کتاب بخوانید. یعنی در حالی که منتظر آماده شدن سفارشتان هستید، کارهای دیگری هم میتوانید انجام دهید.
در برنامهنویسی هم همین اتفاق میافتد. وقتی یک برنامه جاوا اسکریپت کاری را انجام میدهد که زمانبر است (مثل خواندن یک فایل از روی هارد یا درخواست داده از یک سرور)، برنامه میتواند به جای اینکه منتظر بماند تا این کار تمام شود، به سراغ انجام کارهای دیگری برود. اینجاست که مفهوم ناهمزمان وارد میشود.
به زبان سادهتر، ناهمزمان بودن یعنی اینکه یک کار به صورت همزمان با کارهای دیگر انجام شود. در جاوا اسکریپت، بسیاری از کارها به صورت ناهمزمان انجام میشوند تا برنامه سریعتر و روانتر اجرا شود.
برنامه نویسی ناهمزمان (Asynchronous Programming) در جاوا اسکریپت به این معناست که کد شما میتواند بدون انتظار برای اتمام یک عملیات خاص، به اجرای بخشهای بعدی بپردازد. این ویژگی به خصوص در کار با عملیاتهای ورودی/خروجی (مثل خواندن از فایل، درخواست از سرور و ...) بسیار مفید است.
در دنیای برنامهنویسی جاوا اسکریپت، به خصوص در محیطهای مرورگر، بسیاری از عملیاتها به صورت ناهمزمان (Asynchronous) انجام میشوند. این بدین معناست که یک عملیات ممکن است زمان زیادی طول بکشد و در حین انجام آن، برنامه بتواند به اجرای سایر وظایف بپردازد. برای مدیریت این نوع عملیاتها، جاوا اسکریپت از مفهوم Promise و کلمات کلیدی async/await استفاده میکند.
**پس به طور خلاصه، ناهمزمان بودن در جاوا اسکریپت به برنامه اجازه میدهد که در حین انجام کارهای زمانبر، به کارهای دیگری هم بپردازد. این ویژگی باعث میشود برنامهها سریعتر، روانتر و کاربرپسندتر شوند.**
##### چرا برنامه نویسی ناهمزمان مهم است؟
* کارایی بهتر: اگر برنامه شما برای دریافت داده از یک سرور منتظر بماند، در این مدت هیچ کار دیگری انجام نخواهد داد. اما با برنامه نویسی ناهمزمان، برنامه میتواند در همین حین به انجام کارهای دیگر بپردازد.
* رابط کاربری روانتر: در برنامههای تحت وب، اگر درخواستهای سرور زمان زیادی ببرد، رابط کاربری برای کاربر یخ میزند. با ناهمزمانی، کاربر میتواند در حین بارگذاری دادهها، با سایر بخشهای برنامه تعامل داشته باشد.
##### چگونه ناهمزمان بودن را در جاوا اسکریپت پیادهسازی میکنیم؟
جاوا اسکریپت روشهای مختلفی برای پیادهسازی ناهمزمان بودن دارد، از جمله:
* Callbacks: قدیمیترین روش برای مدیریت ناهمزمان بودن است.
* Promises: روشی مدرنتر و قدرتمندتر برای مدیریت ناهمزمان بودن است.
* Async/Await: سینتکسی سادهتر برای کار با Promises است.
##### برنامه نویسی ناهمزمان با استفاده از Callback function
در پیاده سازی کد ناهمزمان با استفاده از Callback، یک تابع به عنوان آرگومان به تابع دیگری پاس داده میشود و زمانی اجرا میشود که عملیات ناهمزمان به پایان برسد.
برای مثال:
```js
function fetchData(callback) {
setTimeout(() => {
const data = { message: 'Hello from the server!' };
callback(data);
}, 2000);
}
fetchData((data) => {
console.log(data);
});
```
در مثال بالا، تابع fetchData یک تابع callback به عنوان آرگومان میگیرد. پس از 2 ثانیه (شبیهسازی یک درخواست به سرور)، تابع callback با دادههای دریافت شده فراخوانی میشود.
##### برنامه نویسی ناهمزمان با استفاده از Promise
Promise در JavaScript یک شیء است که نشاندهنده نتیجه نهایی یک عملیات ناهمزمان است (موفق یا ناموفق).به زبان ساده، Promise یک شیء است که نشاندهنده نتیجه نهایی یک عملیات ناهمزمان است. این عملیات میتواند هر چیزی باشد، از خواندن یک فایل گرفته تا درخواست اطلاعات از یک سرور.
##### چرا به Promise نیاز داریم؟
قبل از Promise، ما از callback ها برای مدیریت عملیاتهای ناهمزمان استفاده میکردیم(مثال بالا). اما Callback ها باعث میشدند کدمان پیچیده و سختخوان شود، مخصوصاً وقتی چندین عملیات ناهمزمان پشت سر هم داشتیم. Promiseها این مشکل را حل کردند و به ما اجازه میدهند کدهای ناهمزمان را به صورت زنجیرهای و خواناتر بنویسیم.
پس پیشنهاد ما به شما این است که همیشه به جای Callback function از Promise استفاده کنید.
یک Promise میتواند در سه حالت مختلف باشد:
* Pending: عملیات هنوز در حال انجام است و نتیجه مشخص نیست.
* Fulfilled: عملیات با موفقیت انجام شده و نتیجه مشخص شده است.
* Rejected: عملیاتی با شکست مواجه شده و خطایی رخ داده است.
مثال:
```js
function fetchData(url) {
return new Promise((resolve, reject) => {
// شبیه سازی ارسال درخواست به سرور
setTimeout(() => {
resolve({data: 'This is server data'});
}, 1000);
});
}
showLoading();
fetchData('https://foo.bar/...')
.then(response => {
console.log(response);// output: {data: 'This is server data'}
})
.catch(error => {
console.log(error);
alert('در دریافت اطلاعات از سرور با خطا مواجه شدیم');
})
.finally(() => {
hideLoading();
});
```
در مثال بالا، ما یک تابع داریم که یک Promise را ساخته و برمیگرداند و در عملیات این Promise درخواست Ajax را شبیه سازی کرده ایم که بعد از 1 ثانیه سرور به ما پاسخ میدهد و data مورد نظر را به ما تحویل میدهد.
همانطور که در مثال بالا مشاهده میکنید برای استفاده از یک Promise، از متدهای then() و catch() استفاده میکنیم.
* then(): زمانی که Promise با موفقیت انجام شد، این تابع فراخوانی میشود.
* catch(): زمانی که Promise با شکست مواجه شد، این تابع فراخوانی میشود.
* finally(): این تابع در هر زمانی اجرا میشود، چه Promise با موفق اجرا شود چه با شکست مواجه شود.
##### مزایای Promise:
* خوانایی بهتر: کدهای ناهمزمان را به صورت زنجیرهای و خواناتر مینویسیم.
* مدیریت بهتر خطاها: با استفاده از catch() میتوانیم خطاهای رخ داده را به راحتی مدیریت کنیم.
* قابلیت ترکیب: میتوانیم چندین Promise را با هم ترکیب کنیم.
##### Async/Await:
**لطفا تا زمانی که Promise را کاملا درک نکرده اید این قسمت را مطالعه نکنید !**
Async و Await دقیقا چی هستند؟ تصور کنید یک تابع داریم که داخل آن چندین Promise را هندل میکنیم:
```js
function foo() {
return new Promise((resolve, reject) => {
fetchData('/data1')
.then(data1 => {
fetchData('/data2')
.then(data2 => {
fetchData('/data3')
.then(data3 => {
resolve({
data1, data2, data3
});
})
.catch(err => {
reject(err);
});
})
.catch(err => {
reject(err);
});
})
.catch(err => {
reject(err);
});
});
}
foo()
.then(allData => {
console.log(allData);// result: {data1: ..., data2: ..., data3: ...}
})
.catch(error => {
alert('خطا');
});
```
در مثال بالا میبینید که ما 3 تا Promise داریم و میخواهیم بعد از اتمام هر Promise، Promise بعدی اجرا گردد و مجبور شدیم کد ها را داخل همدیگر بنویسیم !
همانطور که پیداست کد های ما به شدت شلوغ و بهم ریخته هستند، فرض کنید در این کد با خطا مواجه شویم و بخواهیم کد را دی باگ کنیم، مسلما این کار بشدت دشوار و پیچیده خواهد شد. یا فرض کنید به جای 3 عدد Promise، 10 عدد Promise داشتیم! قطعا با جهنمی از Promise و کد های تودرتو مواجه خواهیم شد!
راه حل چیست؟ JavaScript برای حل این مسئله به ما Async و Await را پیشنهاد میکند.
در صورتی که قبل از هر متغییر یا تابعی که Promise برمیگرداند کلمه await را بگذاریم JavaScript تا اجرا شدن کامل Promise صبر میکند:
```js
const data1 = await fetchData('/data1');
const data2 = await fetchData('/data2');
const data3 = await fetchData('/data3');
console.log({data1, data2, data3});// output: data1: ..., data2: ..., data3: ...
```
همانطور که میبینیدکد های ما بسیار بسیار خواناتر، توسعه پذیر تر، و حجم آنها بسیار کم شده است و خروجی این کد با کد مثال قبلی دقیقا یکسان است !
اما با اجرای این کد در مرورگر با خطا مواجه خواهیم شد!، چرا؟ به این دلیل که در JavaScript یک قانون برای استفاده از await وجود دارد:
**زمانی که در کد خود از await استفاده کنید، await شما حتما باید داخل یک تابع از نوع async باشد:**
```js
async function foo() {
const data1 = await fetchData('/data1');
const data2 = await fetchData('/data2');
const data3 = await fetchData('/data3');
return {data1, data2, data3};
}
foo()
.then(allData => {
console.log(allData);// result: {data1: ..., data2: ..., data3: ...}
})
.catch(error => {
alert('خطا');
});
```
همانطور که در مثال بالا مشخص است، برای این که یک تابع را از نوع async کنید کافیست قبل از کلمه function از کلمه async استفاده کنید.
در واقع کلمه assync به JavaScript میگوید که این تابع میتواند به صورت ناهمگام اجرا شود و دیگر JavaScript به شما خطایی نخواهد داد.
اما یک نکته بسیار ریزی در کد بالا وجود دارد، همانطور که در کد بالا میبینید ما در تابع foo، هیچ Promiseای را return نکرده ایم اما در پایین از then و catch استفاده کرده ایم. چطور چنین چیزی ممکن است؟
**در واقع وقتی تابع شما از نوع async باشد، هر چیزی که داخل تابع شما return شود در قالب یک Promise خواهد بود.**
بنابراین Objectای که داخل تابع foo ، بازگشت داده شده را باید در قالب یک Promise دریافت کنیم.
**پس همیشه به یاد داشته باشید، به طور کلی تمامی تابع های از نوع async خروجی Promise دارند.**
##### Promise.all:
در مثال بالا، ما یک کدی نوشتیم که سه نوع داده را از سرور دریافت میکند (data1, data2, data3)
نکته ای که در این کد وجود دارد این است که این 3 کد به طور همزمان اجرا نمیشوند، بلکه بعد از اینکه data1 به طور کامل دریافت شد سپس data2 دریافت میشود و بعد از آن data3
فرض کنید که هرکدام از 3 داده ای که دریافت میشوند 1 ثانیه طول بکشد، در این صورت برنامه ما بعد از 3 ثانیه هر 3 داده را خواهد داشت.
به نظر شما اینکه هر 3 داده همزمان دریافت شوند و برنامه به طور همزمان منتظر دریافت هر 3 داده باشد و بعد به سراغ کد های بعدی برود بهتر نیست؟!
شاید با خود بگوئید که هدف ما از await این بود که تا زمانی که این Promise به طور کامل اجرا نشده کد بعدی اجرا نشود. حرف شما درست است اما برنامه ما در این مثال نیازی ندارد که حتما اول data1 به طور کامل دریافت شود، بعد data2 و بعد از آن data3.
**بعضی اوقات ما نیاز داریم که برنامه به طور همزمان منتظر چندین Promise باشد و بعد از اینکه به طور همزمان اجرا شدند با موفقیت تکمیل شدند سپس برنامه به سراغ اجرای خط های بعدی برود.**
در این مواقع میتوانیم از Promise.all استفاده کنیم:
```js
async function foo() {
const [data1, data2, data3] = await Promise.all([
fetchData('/data1'),
fetchData('/data2'),
fetchData('/data3')
]);
return {data1, data2, data3};
}
```
مثالی که زده شده با مثالی که بالاتر زده شد دقیقا یکسان است با این تفاوت که این مثال سریع تر انجام خواهد شد.
**در واقع Promise.all یک آرایه ای از Promise ها را میگیرد و خود یک Promise را بازگشت میدهد و زمانی که تمامی Promise هایی که داخل آرایه resolve شدند Promise مربوط به Promise.all هم resolve خواهد شد.**
پس در مثال بالا، همانطور که گفته شد، فرض کنید هرکدام از این دریافت داده ها از سمت سرور 1 ثانیه طول بکشند، در صورتی که از Promise.all استفاده نشود مدت زمان اجرای کد ها 3 ثانیه خواهد بود اما در صورتی که از Promise.all استفاده شود مدت زمان اجرای کد ها 1 ثانیه خواهد بود.
بنابراین، زمانی که چندین Promise پشت سر هم استفاده میکنید طوری که همزمان یا غیرهمزمان بودن آنها برای شما اهمیتی ندارد از Promise.all استفاده کنید تا کد شما سریع تر اجرا شود.