منابع آموزشی و پژوهشی، برای دانشگاه و مدرسه
راهنمای انتخابمیز نویسنده
آموزش javascriptدرس ۱۴ از ۲۰رایگان

Promise و Event Loop در جاوااسکریپت؛ ترتیب اجرای کد

کد ناهمگام لزوماً به ترتیب ظاهری خط‌ها نتیجه نمی‌دهد. در این درس، مدل اجرای سادهٔ مرورگر، Promise و زنجیرهٔ then/catch را با مثال‌های بدون نیاز به شبکه بررسی می‌کنیم و دلیل ترتیب خروجی‌ها را توضیح می‌دهیم.

فضای تمرینبه‌روزرسانی: ۱۴۰۵/۸/۵
کدنویسی ناهمگام و داده
در این درس می‌خوانید

دریافت فایل، پاسخ API و انتظار برای تایمر، همگی کارهایی‌اند که ممکن است نتیجهٔ آن‌ها بعداً آماده شود. مرورگر باید در این فاصله بتواند به کاربر پاسخ بدهد. Promise قراردادی برای دریافت نتیجه یا خطای یک عملیات است؛ به‌تنهایی پردازش سنگین را به یک هستهٔ دیگر منتقل نمی‌کند. در درس چهاردهم، قبل از شبکه و async/await، ترتیب اجرا را با چند مثال کوتاه می‌خوانیم. برای هر مثال یک اجرای تازه در Console داشته باشید.

کد همگام، task و microtask

در یک توضیح مقدماتی از مرورگر، کد همگامِ جاری ابتدا تا پایان اجرا می‌شود. callbackهای Promise از مسیر microtask پردازش می‌شوند و callback تایمر در task بعدیِ مناسب قرار می‌گیرد. این مدل برای نمونهٔ زیر کافی است؛ جزئیات زمان‌بندی در محیط‌های مختلف، به‌ویژه Node.js، یکسان نیست. setTimeout با صفر، اجرای فوری یا تضمین یک زمان دقیق نیست؛ فقط زودترین زمان مناسب را محدود می‌کند.

javascript
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
// ترتیب در این مثال مرورگر: A, D, C, B

A و D در اجرای همگام فعلی چاپ می‌شوند. سپس callback مربوط به Promise فرصت اجرا می‌گیرد و پس از آن تایمر آماده می‌شود. اگر قبل از پایان این کد یک حلقهٔ محاسباتی طولانی بگذارید، هم پیام C و هم پاسخ رابط عقب می‌افتند. همچنین ساختن بی‌پایان microtask می‌تواند کارهای دیگر را معطل کند؛ نوشتن await روی یک Promise ازپیش‌حل‌شده، الزاماً فرصت رندر به صفحه نمی‌دهد.

Promise چه حالت‌هایی دارد؟

  • pending: نتیجه هنوز نهایی نشده است.
  • fulfilled: عملیات با یک مقدار موفق پایان یافته است.
  • rejected: عملیات با یک علت خطا پایان یافته است.
  • settled: اصطلاحی برای fulfilled یا rejected؛ پس از آن نتیجهٔ همان Promise دوباره عوض نمی‌شود.

ساختن Promise معمولاً در مرز یک API مبتنی بر callback لازم می‌شود؛ بسیاری از APIهای مدرن از قبل Promise برمی‌گردانند. executor که به new Promise می‌دهیم هنگام ساخت به‌صورت همگام اجرا می‌شود، ولی handlerهای then روی مسیر ناهمگام زمان‌بندی می‌شوند. استفاده از new Promise(async ...)، یا پیچیدن بی‌دلیل یک Promise آماده در Promise دیگر، معمولاً کار را پیچیده‌تر می‌کند.

یک عملیات شبیه‌سازی‌شده بسازید

javascript
function loadLesson(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (!Number.isInteger(id) || id < 1) {
        reject(new RangeError("Invalid lesson id"));
        return;
      }
      resolve({ id, title: "JavaScript" });
    }, 20);
  });
}
loadLesson(1)
  .then(lesson => console.log(lesson.title))
  .catch(error => console.error(error.message));

این تابع درخواست اینترنتی نمی‌فرستد و فقط تأخیر را با تایمر شبیه‌سازی می‌کند. resolve مقدار موفق و reject خطا را مشخص می‌کند. return پس از reject برای جلوگیری از ادامهٔ منطق همان callback است؛ reject به‌خودی‌خود شبیه return از بدنه نیست. زمان 20 میلی‌ثانیه یک حداقل تقریبی برای تایمر است و ممکن است callback دیرتر اجرا شود.

then یک زنجیرهٔ تازه می‌سازد

then یک Promise تازه برمی‌گرداند. مقدار return شده از handler به مرحلهٔ بعد می‌رسد؛ اگر یک Promise برگردانید، مرحلهٔ بعد منتظر آن می‌ماند. اگر return فراموش شود، نتیجهٔ بعدی ممکن است undefined باشد و بخشی از عملیات خارج از زنجیره رها شود. خطای پرتاب‌شده در handler نیز زنجیره را به مسیر ردشدن می‌برد و catch پایین‌تر می‌تواند آن را بگیرد.

javascript
Promise.resolve(4)
  .then(value => value * 2)
  .then(value => {
    return Promise.resolve(value + 1);
  })
  .then(value => console.log(value)) // 9
  .catch(error => console.error(error.message));

catch خالی می‌تواند خطا را پنهان کند

اگر catch یک مقدار معمولی برگرداند، زنجیرهٔ بعدی می‌تواند دوباره در مسیر موفق ادامه دهد. این رفتار برای fallback عمدی مفید است، اما catch خالی نباید شکست دادهٔ ضروری را به موفقیت ظاهری تبدیل کند. تصمیم بگیرید خطا کجا به کاربر نشان داده، کجا ثبت و کجا دوباره پرتاب شود. Promiseهای شروع‌شده باید مالک روشن داشته باشند؛ رهاکردن آن‌ها می‌تواند به خطای unhandled rejection برسد.

javascript
Promise.resolve()
  .then(() => { throw new Error("آزمون خطا"); })
  .catch(error => {
    console.log(error.message);
    return "نتیجهٔ جایگزین";
  })
  .then(value => console.log(value));
// ابتدا پیام خطا و سپس نتیجهٔ جایگزین چاپ می‌شود.

تمرین: پیش‌بینی کنید، بعد اجرا کنید

پیش از اجرای کد زیر، ترتیب سه پیام را روی کاغذ بنویسید. پاسخ درست start، end و سپس value: 10 است. علت را با «رسیدن جواب دیرتر است» توضیح ندهید؛ این Promise از ابتدا fulfilled است، اما handlerِ then هنوز بعد از کد همگام اجرا می‌شود. سپس یک then دیگر اضافه کنید که دو واحد به مقدار اضافه کند و نتیجهٔ 12 را چاپ کند.

javascript
console.log("start");
Promise.resolve(5)
  .then(value => value * 2)
  .then(value => console.log(`value: ${value}`));
console.log("end");

مرور پیش از درس بعد

  • Promise با چندنخی‌کردن محاسبهٔ JavaScript یکسان نیست.
  • تایمر صفر به معنای اجرای فوری نیست.
  • then و catch خودشان Promise تازه می‌سازند.
  • مقدار یا Promise مورد نیاز مرحلهٔ بعد را return کنید.
  • برای خطای هر زنجیره مسیر رسیدگی مشخص داشته باشید.

منابع انگلیسی

مثال‌های زمان‌بندی برای این مسیر طراحی شده‌اند. در درس بعد، همان قرارداد را با async/await و ترکیب چند عملیات به شکل خواناتر می‌نویسیم.

پیش‌نیاز و محیط اجرای مثال‌ها

درس 14 از ۲۰؛ پیش‌نیاز: درس‌های قبلی مجموعه. کدهای مستقل این درس در Console تازهٔ مرورگر یا فایل JavaScript اجرا می‌شوند؛ نمونه‌ها را جداگانه آزمایش کنید.

پیشرفت روی همین مرورگر ذخیره می‌شود.

یکجا انتخاب کن، راحت‌تر دریافت کن

انتخاب‌های من ۰ منبع

مجموعهٔ مطالعه‌ات را بساز.

کنار مقاله‌ها «انتخاب» را بزنید تا چند فایل را با هم دریافت کنید.

انتخاب مقاله‌های رایگان