آشنایی با برنامهنویسی ناهمگم
کار با عملیاتی که نتیجهشان دیرتر میرسد؛ setTimeout، وعدهها (Promise) و ساختار async/await برای خواندن کد ناهمگم بهشکل همگم.
آنچه در این درس میآموزید
- بفهمید چرا عملیاتی مثل درخواست شبکه نمیتوانند اجرای برنامه را متوقف کنند
- نتیجهٔ عملیات تأخیری را با Promise و then دریافت کنید
- با async/await کد ناهمگم را به شکل خوانا بنویسید و خطاهایش را مدیریت کنید
فهرست مطالب
تا اینجا کدهای ما همیشه خطبهخط و بلافاصله اجرا میشدند. اما بخش بزرگی از کارهای برنامهٔ وب دیر جواب میدهد: دانلود داده از سرور، خواندن یک فایل یا انتظار برای تایپ کاربر. اگر برنامه برای هر دانلودی چند ثانیه میایستاد و کل صفحه منجمد میشد، تجربهٔ کاربری فاجعه میشد. راهحل جاوااسکریپت «برنامهنویسی ناهمگم» (Asynchronous Programming) است: درخواست را بفرست، برو سراغ کار بعدی، و وقتی جواب رسید بقیهٔ کار را ادامه بده. این درس کمی از درسهای قبل سختتر است؛ اگر در نخستین خواندن همهچیز جا نیفتاد، نگران نباشید.
شمارش رویدادها؛ چرا ترتیب عوض میشود؟
سادهترین ابزار ناهمگم، setTimeout است: تابعی را بعد از گذشت مدتی بر حسب میلیثانیه اجرا میکند. حدس بزنید این برنامه چه چاپ میکند:
console.log("first");
setTimeout(() => {
console.log("third");
}, 100);
console.log("second");first second third
حالا هشدار: این برنامه نه ۱۰۰ میلیثانیه صبر میکند و نه به ترتیب نوشتن اجرا میشود. setTimeout فقط یک کار را زمانبندی میکند و برمیگردد؛ خط «second» بلافاصله اجرا میشود و پس از خالیشدن دست مرورگر یا Node از کارهای فوری، ۱۰۰ میلیثانیه بعد، پیام «third» چاپ میشود. این مکانیزم «حلقهٔ رویداد» (Event Loop) نام دارد: یک صف از کارهای تأخیری که فقط وقتی دست آزاد است سراغشان میرود.
این یعنی قانون طلایی: کد ناهمگم بهترتیب زمانیِ پایان اجرا میشود، نه ترتیب نوشتن. و یعنی نمیتوان نتیجهٔ یک کار تأخیری را در خط بعدی بهسادگی گرفت؛ به ابزاری نیاز داریم که «وقتی آماده شد، این کار را بکن» را تمیز بیان کند.
وعدهها؛ Promise
«وعده» (Promise) شیئی است که نمایندهٔ نتیجهای است که هنوز نیامده. مثل رسیدِ سفارش: هنوز کالا دستتان نرسیده، اما یک شیء در دست دارید که میگوید «بهزودی یا نتیجه میآید یا خطا». هر Promise یکی از سه حالت را دارد: در انتظار (Pending)، موفق (Fulfilled) یا ردشده (Rejected).
const promise = new Promise((resolve) => {
setTimeout(() => resolve("Work done"), 50);
});
promise.then((message) => console.log(message));
console.log("Waiting...");Waiting... Work done
توضیح خطبهخط:
- سازندهٔ
Promiseتابعی میگیرد که بلافاصله اجرا میشود؛ درون آن کارِ تأخیری انجام میشود. - تابع
resolveرا مرورگر میدهد؛ صدا زدنش یعنی «وعده عمل شد» و مقدارش نتیجه است. در سوی مقابل،rejectبرای اعلام خطا است. thenمیگوید «وقتی نتیجه رسید این تابع را اجرا کن». خودش بلافاصله برمیگردد و برنامه به خط بعد میرود؛ برای همین «Waiting...» اول چاپ میشود.
سازندگان کتابخانههای مدرن تقریباً همهجا بهجای توابع قدیمیِ «پاسخ بهمحض رسید» (Callback) وعده برمیگردانند؛ Promise زبان مشترک ناهمگمی مدرن است.
async و await
زنجیرهٔ then در کارهای طولانی بههمریخته میشود. ساختار async/await از سال ۲۰۱۷ اجازه میدهد کد ناهمگم را مثل کد معمولی، خطبهخط و خوانا بنویسیم:
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function main() {
console.log("Start");
await wait(50);
console.log("After the wait");
}
main();Start After the wait
دو کلیدواژه جفتاند:
asyncپیش از تعریف تابع یعنی «این تابع وعده برمیگرداند و حق دارد از await استفاده کند».awaitیعنی «منتظر وعده بمان و وقتی نتیجه رسید در همان متغیر بگذار». اجرای تابع از این خط موقتاً متوقف میشود ولی برنامهٔ کلی متوقف نمیشود؛ بقیهٔ صفحه به کارش ادامه میدهد.
همین دو کلیدواژه کد را از زنجیرهٔ then به شکلی میآورد که ذهن ما سادهترش را میفهمد.
مدیریت خطا در کد ناهمگم
کار تأخیری ممکن است شکست بخورد: شبکه قطع شود یا سرور خطا بدهد. وقتی تابعی reject میشود، await همانجا خطا پرتاب میکند و میتوانید مثل کد همگم با try/catch بگیریدش:
async function risky() {
throw new Error("Something went wrong");
}
risky().catch((error) => console.log("Caught:", error.message));Caught: Something went wrong
تابع async که با throw شکست بخورد، وعدهای ردشده برمیگرداند؛ .catch تابع دریافت خطا را وصل میکند. داخل خود تابعِ async نیز try/catch در اطراف await کار میکند. خطاهای ناهمگم را هرگز بیرسید رها نکنید؛ برنامه بیصدا از کار میافتد و کاربر فقط یک چرخ دندهٔ همیشگی میبیند.
درخواست شبکه با fetch
همهٔ این ابزارها برای یک هدف روزمره ساخته شدهاند: گرفتن داده از سرور. متد fetch وعدهای برای پاسخ HTTP میدهد و خود پاسخ هم وعدهٔ دیگری برای بدنهٔ JSON دارد:
async function loadUser() {
const response = await fetch("https://api.example.com/user/1");
const data = await response.json();
console.log(data.name);
}دو await پشت سر هم دو انتظار را مدیریت میکنند: نخست رسیدن سرآیندهای پاسخ، بعد تبدیل بدنه به شیء جاوااسکریپت — همان اشیاء و آرایههایی که در درسهای قبلی خواندیم. کد واقعی را با یک سرور واقعی در پروژههای بعدی تمرین خواهید کرد؛ الگوی کلی همین است.
تمرینها
تمرین
تمرین ۱ — پیام تأخیری
تابعی به نام waitAndPrint بنویسید که یک رشته بگیرد و پس از ۵۰ میلیثانیه آن را چاپ کند؛ این تابع باید یک وعده برگرداند تا بتوانید آن را با await یا then بهکار ببرید. سپس آن را با پیام «Hello, delayed!» فراخوانی کنید.
// Write your code hereنمایش راهحل
function waitAndPrint(message) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(message);
resolve();
}, 50);
});
}
waitAndPrint("Hello, delayed!");Hello, delayed!
::: :::
خلاصه
- کارهای تأخیری مثل درخواست شبکه نباید اجرای برنامه را متوقف کنند؛ نتیجهشان بعداً در صف رویداد رسیدگی میشود.
setTimeoutکار را زمانبندی میکند و کد ناهمگم به ترتیب زمانی پایان اجرا میشود، نه ترتیب نوشتن.- Promise نمایندهٔ نتیجهای آینده است با سه حالت: در انتظار، موفق و ردشده؛
thenبه موفقیت وcatchبه شکست رسیدگی میکند. async/awaitکد ناهمگم را خطبهخط و خوانا میکند و خطایش باtry/catchمدیریت میشود.fetchپاسخ شبکه را بهشکل وعده میدهد و ستون کار با APIها در برنامههای واقعی است.
و با این درس، مسیر مقدماتی جاوااسکریپت کامل شد: از متغیرها تا برنامهای که صفحه را میبیند، به کاربر گوش میدهد و با دنیای بیرون حرف میزند. قدم بعدی پیشنهاد ما ساخت یک پروژهٔ کوچک واقعی است — مثلاً فهرست کارها با ذخیره در مرورگر — تا همهٔ این مفاهیم را در یک کد واحد به هم بدوزید.