Faragiri

همهٔ زبان‌ها

آموزش جاوااسکریپت ناهمگام جاوااسکریپت

سه درخواست داخل forEach با await، و بلافاصله بعدش آرایه خالی است. همان کد با for..of منتظر می‌ماند و همان کد با Promise.all موازی می‌شود.

۴بخش
۳دقیقه خواندن
۴عمق

یک پرسش، دو شکل نوشتن

چه چیزی تعیین می‌کند یکی‌یکی بنویسید یا با هم

پرسش درباره سرعت نیست؛ درباره این است که هر کار به نتیجه کار قبلی نیاز دارد یا نه.

کار بعدی به نتیجه کار قبلی نیاز دارد؟
  • بله
    یکی‌یکی، با for..ofهر تکرار منتظر می‌ماند و ترتیب تضمین شده است
    • کل زمان، جمع همه انتظارهاست
    • اولین شکست همان‌جا حلقه را متوقف می‌کند
  • خیر
    با هم، با Promise.allهمه شروع می‌شوند و نتیجه‌ها به‌ترتیب ورودی برمی‌گردند
    • کل زمان، طولانی‌ترین انتظار است
    • برای اینکه شکست یکی بقیه را نبرد، allSettled

شاخه‌ای که در این نمودار نیست forEach است، چون اصلا در این پرسش شرکت نمی‌کند: نه منتظر می‌ماند و نه چیزی برمی‌گرداند.

این سه سطر کار نمی‌کنند و هیچ خطایی هم نمی‌دهند: یک forEach با تابع async داخلش، که هر بار نتیجه‌ای را به آرایه اضافه می‌کند. بلافاصله پس از forEach آرایه [] است.

دلیلش این است که forEach هیچ‌وقت طوری نوشته نشده بود که منتظر بماند. هر بار تابع را صدا می‌زند، یک پرامیس می‌گیرد، دورش می‌ریزد و به بعدی می‌رود. await فقط داخل همان تابع کار می‌کند و نه در تابعی که صدایش زده.

سه راه درست، و تفاوتشان تصمیم شماست و نه سبک:

  • for (const k of kodha) { ... await ... } یکی‌یکی، به‌ترتیب، و کند
  • await Promise.all(kodha.map(bekhan)) هر سه با هم، و نتیجه‌ها به‌ترتیب ورودی: ["a","b","c"]
  • await Promise.allSettled(...) وقتی می‌خواهید یک شکست بقیه را متوقف نکند

ترتیب پایان، ترتیب نتیجه نیست

اگر داخل همان کارهای موازی خودتان به یک آرایه اضافه کنید، ترتیبی که می‌گیرید ترتیب پایان است: با تاخیرهای سی و پنج و پانزده میلی‌ثانیه، خروجی ["b","c","a"] می‌شود. اما مقدار برگشتی Promise.all همیشه به‌ترتیب ورودی است. این دو را قاطی کردن، رایج‌ترین باگ این صفحه است و روی شبکه سریع اصلا خودش را نشان نمی‌دهد.

Promise.all با اولین شکست بلافاصله رد می‌شود و بقیه را رها می‌کند، که برای بارگذاری بخش‌های یک صفحه معمولا اشتباه است. Promise.allSettled منتظر همه می‌ماند و برای هرکدام وضعیت برمی‌گرداند، پس می‌شود سه بخش را نشان داد و برای چهارمی پیام خطا گذاشت.

یک تابع async همیشه یک پرامیس برمی‌گرداند

حتی وقتی بدنه‌اش هیچ انتظاری ندارد و فقط یک عدد برمی‌گرداند، مقدار برگشتی یک پرامیس است. پیامدش این است که فراموش کردن await خطا نمی‌دهد: مقدار را جایی می‌گذارید و بعدا با یک شی عجیب روبه‌رو می‌شوید، یا شرطی می‌نویسید که همیشه درست است چون هر پرامیسی مقدار درست‌نما دارد.

و خطا در کد ناهمگام فقط با try دور خودِ await گرفته می‌شود. یک try که کار را شروع می‌کند ولی منتظرش نمی‌ماند هیچ چیزی نمی‌گیرد، و آن رد شدن به‌شکل یک خطای مدیریت‌نشده بیرون می‌آید، که همان مرزی است که شکست پرصدا، شکست بی‌خطر، و فرق این دو می‌کشد.

جایی که کار گیر می‌کند، شبکه‌ای است که کند است

روی اتصالی که از ایران به سرویس‌های بیرونی می‌رود، تاخیر بالاست و پایدار هم نیست. همین باعث می‌شود دو باگ که در جای دیگر ماه‌ها پنهان می‌مانند، همان روز اول بیرون بزنند: کدی که ترتیب پایان را با ترتیب ورودی یکی گرفته، و کدی که وقفه زمانی ندارد و برای همیشه منتظر می‌ماند. عادتی که هر دو را می‌گیرد یکی است: هر فراخوانی شبکه را با یک وقفه زمانی صریح بنویسید. در مرورگر و در Node امروزی، AbortSignal.timeout این را در یک آرگومان می‌دهد و جای نوشتن دستی یک تایمر و یک پرچم را می‌گیرد.

ابزار، و سالی که این حرف درست است

در سال ۲۰۲۶ fetch در خود مرورگر و در Node هست و جای XMLHttpRequest و بسته‌های جانبی درخواست را گرفته؛ رایگان است و چیزی لازم ندارد. یک دام واقعی دارد که باید بدانید: پاسخ با کد خطا هم پرامیس را رد نمی‌کند، پس باید خودتان response.ok را بررسی کنید، وگرنه صفحه خطای سرور را به‌عنوان داده معتبر پردازش می‌کنید. برای دیدن اینکه کدام درخواست چقدر طول کشیده و کدام‌ها موازی رفته‌اند، برگه شبکه در بازرس مرورگر همین را به‌شکل نمودار نشان می‌دهد.

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

کنار این

بالای این