مقدمه
در روزهای اولیه اینترنت، وب سایت ها اغلب از داده های ثابت در یک صفحه HTML تشکیل می شدند. اما اکنون که برنامههای کاربردی وب تعاملیتر و پویاتر شدهاند، انجام عملیات فشرده مانند درخواستهای شبکه خارجی برای بازیابی دادههای API به طور فزایندهای ضروری شده است. برای انجام این عملیات در جاوا اسکریپت، یک توسعه دهنده باید از تکنیک های برنامه نویسی ناهمزمان استفاده کند.
از آنجایی که جاوا اسکریپت یک زبان برنامه نویسی تک رشته ای با یک مدل اجرای همزمان است که عملیات را یکی پس از دیگری پردازش می کند، در هر زمان فقط می تواند یک دستور را پردازش کند. با این حال، اقدامی مانند درخواست داده از یک API بسته به اندازه داده درخواستی، سرعت اتصال به شبکه و سایر عوامل میتواند زمان نامشخصی را ببرد. اگر تماسهای API بهصورت همزمان انجام میشد، مرورگر نمیتواند هیچ ورودی کاربر، مانند پیمایش یا کلیک کردن روی یک دکمه را تا زمانی که آن عملیات کامل شود، مدیریت کند. این به عنوان مسدود کردن شناخته می شود.
به منظور جلوگیری از رفتار مسدود کردن، محیط مرورگر دارای API های وب زیادی است که جاوا اسکریپت می تواند به آنها دسترسی داشته باشد که ناهمزمان هستند، به این معنی که می توانند به جای اجرای متوالی، موازی با سایر عملیات ها اجرا شوند. این مفید است زیرا به کاربر اجازه می دهد تا زمانی که عملیات ناهمزمان در حال پردازش است، به استفاده از مرورگر به طور معمول ادامه دهد.
حلقه رویداد
این بخش توضیح می دهد که چگونه جاوا اسکریپت کدهای ناهمزمان را با حلقه رویداد مدیریت می کند. ابتدا از طریق نمایشی از حلقه رویداد در محل کار اجرا می شود و سپس دو عنصر حلقه رویداد را توضیح می دهد: پشته و صف.
کد جاوا اسکریپتی که از هیچ یک از APIهای وب ناهمزمان استفاده نمیکند، به صورت همزمان اجرا میشود—یک در یک زمان و به صورت متوالی. این با این کد مثال نشان داده می شود که سه تابع را فراخوانی می کند که هر کدام یک عدد را در کنسول چاپ می کنند:
// Define three example functions
function first() {
console.log(۱)
}
function second() {
console.log(۲)
}
function third() {
console.log(۳)
}
در این کد سه تابع تعریف می کنید که با console.log() اعداد را چاپ می کند.
سپس، فراخوانی ها را به توابع بنویسید:
// Execute the functions
first()
second()
third()
خروجی بر اساس ترتیب فراخوانی توابع خواهد بود—first()، second()، سپس three():
Output
۱
۲
۳
هنگامی که از یک Web API ناهمزمان استفاده می شود، قوانین پیچیده تر می شوند. یک API داخلی که میتوانید با آن تست کنید setTimeout است که یک تایمر تنظیم میکند و یک عمل را بعد از مدت زمان مشخص انجام میدهد. setTimeout باید ناهمزمان باشد، در غیر این صورت کل مرورگر در طول انتظار ثابت می ماند که منجر به تجربه کاربری ضعیف می شود.
برای شبیه سازی درخواست ناهمزمان، setTimeout را به تابع دوم اضافه کنید:
// Define three example functions, but one of them contains asynchronous code
function first() {
console.log(۱)
}
function second() {
setTimeout(() => {
console.log(۲)
}, ۰)
}
function third() {
console.log(۳)
}
setTimeout دو آرگومان می گیرد: تابعی که به صورت ناهمزمان اجرا می شود و مدت زمانی که قبل از فراخوانی آن تابع منتظر می ماند. در این کد شما console.log را در یک تابع ناشناس قرار دادید و آن را به setTimeout ارسال کردید، سپس تابع را تنظیم کنید تا بعد از ۰ میلی ثانیه اجرا شود.
حالا مانند قبل، توابع را فراخوانی کنید:
// Execute the functions
first()
second()
third()
ممکن است انتظار داشته باشید با تنظیم setTimeout روی ۰، اجرای این سه عملکرد همچنان منجر به چاپ اعداد به ترتیب متوالی شود. اما از آنجایی که ناهمزمان است، تابع با وقفه در آخرین بار چاپ می شود:
Output
۱
۳
۲
اینکه زمانبندی را روی صفر ثانیه یا پنج دقیقه تنظیم کنید، تفاوتی نمیکند – console.log که با کد ناهمزمان فراخوانی میشود، پس از عملکردهای سطح بالای همزمان اجرا میشود. این به این دلیل اتفاق می افتد که محیط میزبان جاوا اسکریپت، در این مورد مرورگر، از مفهومی به نام حلقه رویداد برای رسیدگی به رویدادهای همزمان یا موازی استفاده می کند. از آنجایی که جاوا اسکریپت می تواند تنها یک دستور را در یک زمان اجرا کند، به حلقه رویداد نیاز دارد تا از زمان اجرای کدام دستور خاص مطلع شود. حلقه رویداد این را با مفاهیم پشته و صف مدیریت می کند.
پشته
پشته یا پشته تماس، وضعیت عملکردی را که در حال حاضر اجرا میشود، نگه میدارد. اگر با مفهوم پشته آشنا نیستید، میتوانید آن را بهعنوان آرایهای با ویژگیهای «Last in, first out» (LIFO) تصور کنید، به این معنی که فقط میتوانید موارد را از انتهای پشته اضافه یا حذف کنید. جاوا اسکریپت فریم فعلی (یا فراخوانی تابع در یک محیط خاص) را در پشته اجرا می کند، سپس آن را حذف می کند و به فریم بعدی می رود.
برای مثالی که فقط حاوی کد همزمان است، مرورگر اجرا را به ترتیب زیر انجام می دهد:
- first() را به پشته اضافه کنید، first() را اجرا کنید که ۱ را در کنسول ثبت می کند، first() را از پشته حذف کنید.
- second() را به پشته اضافه کنید، second() را اجرا کنید که ۲ را به کنسول وارد می کند، second() را از پشته حذف کنید.
- third () را به پشته اضافه کنید، سوم () را اجرا کنید که ۳ را در کنسول ثبت می کند، سوم () را از پشته حذف کنید.
مثال دوم با setTimout به شکل زیر است:
- first() را به پشته اضافه کنید، first() را اجرا کنید که ۱ را در کنسول ثبت می کند، first() را از پشته حذف کنید.
- second() را به پشته اضافه کنید، second() را اجرا کنید.
- setTimeout() را به پشته اضافه کنید، setTimeout() API Web را اجرا کنید که تایمر را شروع می کند و تابع ناشناس را به صف اضافه می کند، setTimeout() را از پشته حذف کنید.
- second() را از پشته حذف کنید.
- سوم () را به پشته اضافه کنید، سوم () را اجرا کنید که ۳ را در کنسول ثبت می کند، سوم () را از پشته حذف کنید.
- حلقه رویداد صف را برای هر پیام معلق بررسی میکند و تابع ناشناس را از setTimeout() مییابد، تابع را به پشته اضافه میکند که ۲ را در کنسول ثبت میکند، سپس آن را از پشته حذف میکند.
با استفاده از setTimeout، یک Web API ناهمزمان، مفهوم صف را معرفی می کند که این آموزش در ادامه به آن می پردازد.
صف
صف که به آن صف پیام یا صف وظیفه نیز گفته می شود، یک منطقه انتظار برای توابع است. هر زمان که پشته تماس خالی باشد، حلقه رویداد صف را برای هرگونه پیام در انتظار، از قدیمی ترین پیام، بررسی می کند. هنگامی که یکی را پیدا کرد، آن را به پشته اضافه می کند، که تابع موجود در پیام را اجرا می کند.
در مثال setTimeout، تابع ناشناس بلافاصله پس از بقیه اجرای سطح بالا اجرا می شود، زیرا تایمر روی ۰ ثانیه تنظیم شده بود. مهم است که به خاطر داشته باشید که تایمر به این معنی نیست که کد دقیقاً در ۰ ثانیه یا هر زمان مشخصی اجرا می شود، بلکه به این معنی است که تابع ناشناس را در این مدت زمان به صف اضافه می کند. این سیستم صف به این دلیل وجود دارد که اگر تایمر بخواهد تابع ناشناس را مستقیماً به پشته پس از اتمام تایمر اضافه کند، عملکردی را که در حال حاضر در حال اجرا است قطع میکند، که میتواند اثرات ناخواسته و غیرقابل پیشبینی داشته باشد.
نکته: همچنین یک صف دیگر به نام صف شغل یا صف میکروتسک وجود دارد که به وعده ها رسیدگی می کند. وظایف خرد مانند وعدهها با اولویت بالاتری نسبت به وظایف کلان مانند setTimeout انجام میشود.
اکنون می دانید که چگونه حلقه رویداد از پشته و صف برای رسیدگی به ترتیب اجرای کد استفاده می کند. کار بعدی این است که بفهمید چگونه ترتیب اجرا را در کد خود کنترل کنید. برای انجام این کار، ابتدا با روش اصلی برای اطمینان از مدیریت صحیح کد ناهمزمان توسط حلقه رویداد آشنا خواهید شد: توابع پاسخ به تماس.
Callback Functions
در مثال setTimeout، تابع دارای مهلت زمانی پس از هر چیزی در زمینه اصلی اجرای سطح بالا اجرا شد. اما اگر میخواهید مطمئن شوید که یکی از توابع، مانند تابع سوم، پس از اتمام زمان اجرا میشود، باید از روشهای کدگذاری ناهمزمان استفاده کنید. مهلت در اینجا می تواند یک فراخوانی API ناهمزمان را نشان دهد که حاوی داده است. شما می خواهید با داده های فراخوانی API کار کنید، اما ابتدا باید مطمئن شوید که داده ها برگردانده می شوند.
راه حل اصلی برای مقابله با این مشکل استفاده از توابع برگشت تماس است. توابع پاسخ به تماس، نحو خاصی ندارند. آنها فقط تابعی هستند که به عنوان آرگومان به تابع دیگری ارسال شده است. تابعی که تابع دیگری را به عنوان آرگومان می گیرد، تابع مرتبه بالاتر نامیده می شود. طبق این تعریف، هر تابعی اگر به عنوان آرگومان ارسال شود، می تواند تبدیل به تابع فراخوانی شود. تماسهای تلفنی ذاتاً ناهمزمان نیستند، اما میتوانند برای مقاصد ناهمزمان استفاده شوند.
در اینجا یک مثال کد نحوی از یک تابع مرتبه بالاتر و یک پاسخ تماس وجود دارد:
// A function
function fn() {
console.log('Just a function')
}
// A function that takes another function as an argument
function higherOrderFunction(callback) {
// When you call a function that is passed as an argument, it is referred to as a callback
callback()
}
// Passing a function
higherOrderFunction(fn)
در این کد یک تابع fn تعریف میکنید، یک تابع aboveOrderFunction تعریف میکنید که یک تابع callback را به عنوان آرگومان میگیرد و fn را بهعنوان یک callback به بالاترOrderFunction میدهید.