جاوااسکریپت چجوری کار میکنه؟ ایونت لوپ و قدرت گرفتن برنامهنویسی async و ۵ روش برای بهتر کد نوشتن به کمک async/await - قسمت اول
سری "جاوااسکریپت چجوری کار میکنه؟" بازگردان مقالههای Alexander Zlatkov به زبون خودم هست. هدف از این کار در مرحلهی اول یادگیری خودم و انتشار این مقاله به فارسیه.
قرارم بر این نیست که همهی کلمهها رو به فارسی بگم، به این دلیل که اگر خواستید بیشتر در مورد کلمات کلیدیش جستجو داشته باشید، بتونید راحتتر این کار رو انجام بدین.
اهمیت این نوشته برای من محتواش هست نه طرز بیان یا نگارشش. ممنون میشم اگر در محتوا یا در طرز بیان یا نگارش اشکالی هست بیان کنید تا بتونم بهترش کنم.
بازگردانی شده از: How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await
این بار با مروری بر مشکلات برنامهنویسی روی محیطی تک تردی و معرفی راه حلهای این مشکل برای ساخت یک UI خفن؛ گفتههای خودمون رو در نوشتهی اول این سری تکمیل میکنیم و در آخر هم طبق رسم همیشگی ۵ نکته در مورد تمیزتر کد زدن به کمک async/await میگیم.
چرا تک تردی بودن محدودیت به حساب میاد؟
تو اولین نوشته رو این سؤال فکر کردیم که چه اتفافی میاوفته وقتی تابعی در call stack، زمان زیادی برای اجرا بخواد؟ مثلا اجرای یه الگوریتم سنگین پردازش عکس تو مرورگر.
هر زمانی که call stack تابعی رو در حال اجرا داشته باشه مرورگر هیچ کار دیگهای نمیتونه انجام بده و بلاک شده. این به این معنی هست که نمیتونه صفحات رو رندر کنه یا کد دیگهای رو اجرا کنه و به طور کامل گیر کرده و اینجاست که مشکل خودش رو نشون میده. اپلیکیشن ما دیگه UI بهینه و جذابی نداره.
شاید تو بعضی از کیسها چیز مورد داری نباشه ولی ما یه مشکل بزرگتر دیگهای داریم. وقتی مرورگر ما شروع به اجرای کلی کار زیاد از call stack میکنه، باعث میشه مرورگر مدت زیادی نسبت به امور موازی با جاوااسکریپت واکنش نشون نده. (رندرینگ UI و اجرای جاوااسکریپت روی یک ترد انجام میشه). تو این حالت اکثر مرورگرها اروری نمایش میدن که حاوی این درخواست از کاربر هست که این صفحه رو ببندن یا نگه دارن. فاجعست. هم زشته هم کل UX رو بهم ریخته.
اجزای سازنده ی جاوا اسکریپت
ممکنه شما اپلیکیشن جاوااسکریپتتون رو توی یه فایل .js بنویسید ولی قطعاًً همون برنامه هم از قسمتهای مختلفی تشکیل شده که هر کدومشون زمان اجرای مختلفی دارن و فقط یه کدوم از اونها در لحظه در حال اجرا هست و بقیه بعداً اجرا میشن. رایجترین اجزای سازندهی برنامههای ما توابع هستن.
مشکل اکثر دولوپرهایی که تازه با جاوااسکریپت آشنا شدن در درک ترتیب اجرای برنامه هست. گاهی ممکنه کدی که در لاین یک نوشتیم بعد از اتمام همهی لاینها اجرا بشه. در حقیقت کدهایی که الان در حال اجرا هستند ولی اتمام اجرای اونها برای الان نیست رو تسکهای اسینکرنس (asynchronous) میگن که این سبک دستورهای اجرایی، مشکلی که در بالا بهش اشاره کردیم رو ندارن (بلاک کردن کل مرورگر ).
به تکه کد پایین نگاه کنید:
// ajax(..) is some arbitrary Ajax function given by a library
var response = ajax('https://example.com/api');
console.log(response);
// `response` won't have the response
خب احتمالاًً میدونید یک درخواست ایجکس استاندارد، سینکرنسلی (synchronously) اجرا نمیشه و خب طبق این گفته تابع ajax جواب مورد نظر مارو return نمیکنه تا متغیر response اختصاص داده بشه.
سادهترین راه برای صبر کردن برای جواب یک تابع اسنیکرنس استفاده از کالبکها هست.
ajax('https://example.com/api', function(response) {
console.log(response); // `response` is now available
});
در حد یک نکتهی کوچیک: البته ما میتونیم درخواستهای ایجکس سینکرنس بسازیم. هیچوقت این کار رو نکنید. اگر همچین کاری کنیم UI رو به طور کامل بلاک کردین و کاربر نمیتونه جایی کلیک کنه، اسکرول کنه، دیتا وارد کنه و یا به صفحهی دیگهای جابجا بشه.
به طور کلی تراکنش کاربر با مرورگر رو مختل میکنه و کار توصیه شده و درستی نیست.
با این یه تیکه کد میتونید درخواست سینکرنس درست کنید که استفاده از این کد توصیه نمیشه.
// This is assuming that you're using jQuery
jQuery.ajax({
url: 'https://api.example.com/endpoint',
success: function(response) {
// This is your callback.
},
async: false // And this is a terrible idea
});
ما فقط از درخواست ایجکس به عنوان یه مثال صحبت کردیم ولی میشه همهی کدها رو به طور اسینکرنس اجرا کرد، اون هم به کمک تابع setTimeout. کاری که این تابع انجام میده یک رویداد رو ایجاد میکنه که در چند لحظهی بعدی قرار هست اجرا بشه. به این تکه کد نگاه کنید:
function first() {
console.log('first');
}
function second() {
console.log('second');
}
function third() {
console.log('third');
}
first();
setTimeout(second, 1000); // Invoke `second` after 1000ms
third();
خروجی این کد این شکلیه
first
third
second
کالبد شکافی Event loop
بذارید این مبحث رو با یه ادعای عجیب شروع کنیم. تا قبل از ES6 خود زبان جاوااسکریپت مفهومی اسینکرنس بودن رو "به طور مستقیم" در خودش نداشته هر چند که امکان اجرای کدهای اسینکرنس رو به کمک setTimeout داشت. موتور اجرایی جاوااسکریپت در حقیقت فقط وظیفهای جز اجرای یه مشت دستور که در هر زمان بهش داده میشه رو داره.
برای اطلاع بیشتر از نحوهی کار کردن موتور V8 به پست قبلی ما مراجعه کنید.
خب پس چه چیزی به موتور اجرایی جاوااسکریپت دستور انجام برنامههای شما رو میده؟ در واقع موتور جاوا اسکپریت به طور ایزوله اجرا نمیشه و توسط یه میزبان مورد استفاده قرار میگیره، که این میزبانها در چشم ما دولوپرها بیشتر مرورگرها و Nodejs هستن. ولی خب الان جاوااسکریپت همه جا سرک کشیده. از روباتها و یا چراغها که هر کدوم اینها یک میزبان متفاوت برای موتور جاوااسکریپت به حساب میان.
نقطهی مشترک همهی این میزبانها یک مکانیزمی هست به نام event loop، که کارش هندل کردن اجرای کدهای جاوااسکریپتی هست که با هر بار فراخوان کردن موتور جاوااسکریپت این کار صورت میگیره.
این به این معنی هست که موتور جاوااسکریپت یه محیط اجرایی کد هست که در هر لحظه میشه کدهای جورواجور جاوااسکریپت رو بهش داد تا اونها رو اجرا کنه و این میزبان هست که ایونتها و جریان اجرای کدها رو در کنترل خودش داره.
برای مثال وقتی کد شما درخواست ایجکسی رو برای دریافت اطلاعات به سمت سرور میفرسته شما در قالب یه تابع کالبک منتظر جواب از سمت سرور میمونید. در همین لحظه موتور اجرایی به میزبان میگه من دیگه فعلاًً کارها رو متوقف میکنم. هر وقت جواب رو از سرور گرفتی این تابع کالبکی رو که مشخص کردم برات فراخوانی کن.
مرورگر به عنوان میزبان یه لیسنر ایجاد میکنه برای دریافت جواب از نتورک، و هر وقت که جوابی رو دریافت کنه از سمت نتورک، تابع کالبکی رو که نوشته بودیم برای اجرا به event loop برمیگردونه.
به دیاگرام زیر دقت کنید:
میتونید در مورد memory leak و call stack در مقالهی قبلی بیشتر بخونید.
تو دیاگرام یه قسمتی هست با عنوان Web APIs. در واقع اینها تردهایی هستن که بهشون دسترسی مستقیم نداریم ولی میتونیم اونها رو صدا بزنیم. اونها قسمتی از مرورگر هستن که کنکارنسی رو فراهم میکنن. و اگر شما هم Node.js دولوپر باشید و میزبان رو نودجیاس در نظر بگیریم این ها apiهایی هستن که توسط زبان c++ فراهم شدن.
خب بریم سراغ اصل مطلب. event loop چی هست؟
ایونت لوپ فقط یه کار ساده داره. اونم مانیتور کردن call stack و callback queue. اینجوری کار میکنه که هر وقت call stack رو خالی ببینیه از صف کالبکها یه دونه برمیداره و به call stack اضافه میکنه و در جا اجراش میکنه.
در event loop به هر بار تکرار این اتفاق یه tick گفته میشه و هر آیتم درون صف یک event نام داره که در حقیقت یه تابع callback هست.
console.log('Hi');
setTimeout(function cb1() {
console.log('cb1');
}, 5000);
console.log('Bye');
بیاید این کد رو اجرا کنیم و بررسی کنیم چه اتفاقاتی داره میاوفته:
۱- وضعیت مشخصه. call stack خالیه. console مرورگر هم خالیه.
۲- کال استک با اولین خط پر شده
console.log('Hi')
۳- خط اول اجرا شد
۴- خط اول از call stack حذف شد.
5- خطی که شامل setTimeout هست وارد callstack میشه
۶- خب setTimeout اجرا میشه و مرورگر یه تایمری رو که قسمتی از web api هست برای ما راه میندازه. این تایمر قرار شمارش معکوس تا اجرای اون کالبکی که به setTImeout دادیم رو هندل کنه.
۷- خود setTimeout کارش تموم شده و باید از call stack حذف بشه.
۸ - خطر آخر به call stack اضافه شده
console.log('Bye')
۹- خط آخر اجرا شد
۱۰ - خب بعد از اجرا هم باید از callstack حذف بشه
11- بعد از حداقل ۵۰۰۰ میلی ثانیه تایمر کارش تموم میشه و خود تایمر تابع کالبک setTimeout رو به call stack منتقل میکنه.
۱۲- حالا نوبت cb1 هست. event loop این تابع کالبک رو از صف کالبک ها خارج میکنه و به call stack منتقل میکنه.
۱۳- تابع cb1 اجرا میشه و در ادامش اخرین دستور کنسول هم به call stack اضافه میشه.
۱۴- این دستور اجرا میشه
console.log('cb1')
۱۵- تابع log از call stack حذف میشه
۱۶- حالا نوبت cb1 هست که از call stack حذف بشه
یه نگاه سریع به همه این ۱۶ تا اتفاق
جالبه بدونید داستان تو ES6 تغییر کرده به این صورت که موتور جاوااسکریپت هست که مشخص میکنه event loop باید چجوری کار کنه و ازش استفاده بشه. این یه جورایی داره میگه event loop تنها و تنها جزو وظایف میزبان (مروگر، Node.js) به حساب نمیاد و یه قسمتی از اون سمت جاوااسکریپت سوق داده شده. یکی از مهمترین دلایل این تغییر معرفی Promise هاست. برای این قابلیت نیاز به کنترل همه جانبهی event loop یک ضرورت به حساب میاد. در مورد این موضوع بیشتر توضیح خواهیم داد.
تابع setTimeout چه جوری کار میکنه؟
نکتهی مهم در مورد setTimeout اینه که به صورت اتوماتیک تابع کالبک ما رو وارد صف event loop نمیکنه. یه تایمر درست میشه و هر وقت که تایمر منقضی بشه میزبان تابع کالبک ما رو تازه وارد صف میکنه و تابع باید صبر کنه تا فرآیند tick اتفاق بیافته تا از صف event loop به call stack منتقل بشه.
setTimeout(myCallback, 1000);
معنی کد بالا این نیست که myCallback دقیقاً بعد از ۱۰۰۰ میلیثانیه اجرا میشه. در حقیقت بعد از این مدت myCallback تازه به صف event loop اضافه میشه. و خب تازه آخر داستان نیست. اگر تو صف event loop توابع دیگه زودتر تو صف وارد شده باشن اجرای تابع myCallback بیشتر به تعویق میافته.
الان که میدونیم event loop چیه و setTimeout چجوری کار میکنه استفاده از setTimeout با زمان ۰ میلی ثانیه به عنوان پارامتر دوم به این معنی میشه که callback که به عنوان پارامتر اول پاس دادیم بعد از خالی شدن call stack اجرا میشه
setTimeout(callback, 0)
به کد زیر نگاه کنید:
console.log('Hi');
setTimeout(function() {
console.log('callback');
}, 0);
console.log('Bye');
با وجود این که زمان رو روی ۰ میلیثانیه تنظیم کردیم ولی خروجی مرورگر جور دیگهای هست
Hi
Bye
callback
المان جدیدی به نام Job و Job Queue در جاوا اسکریپت
مفهوم جدیدی به نام Job Queue در ES6 معرفی شده که یک لایه بالای event loop هست که احتمالاً وقتی با Promiseها کار میکردین بهش برخوردین.
فعلاًً در مورد این قضیه یکم صحبت میکنیم تا بتونیم وقتی در مورد برنامهنویسی اسینک با پرامیس بحث میکنیم متوجه نحوهی مدیریت و اجرای اونها بشیم.
اینجوری تصورش کنید که صف Jobها به انتهای هر tick از صف event loop متصل هست. بعضی از اکشنهای اسینک در زمان انجام tick باعث ایجاد یه ایونت جدید نمیشن و به جای اون یک آیتم جدید Job به انتهای Job queue اضافه میشه.
این به این معنی هست که ما میتونیم کدهامون رو بعدا اجرا کنیم و مطمئن باشیم قبل از هر چیز دیگهای اجرا میشن.
به مثال زیر دقت کنید.
var promise = new Promise(function(resolve, reject) {resolve(1)});
promise.then(function(resolve) {console.log(1)});
console.log('a');
promise.then(function(resolve) {console.log(2);});
setTimeout(function() {console.log('h')}, 0);
promise.then(function(resolve) {console.log(3)});
console.log('b');
// a
// b
// 1
// 2
// 3
// h
اول کدهای سینک اجراشون تموم میشه و نوبت تیک event loop میرسه. سه تا Job با استفاده از Promise.then ها به Job queue اضافه شده که به صورت FIFO به call stack اضافه و اجرا میشن. و بعد از خالی شدن صف Job queue نوبت اجرای تیک هست و برداشتن تابع کالبک setTimeout از صف event loop به call stack. (به Jobهایی که توسط promise.then اضافه میشن PromiseJob میگن. چند نوع Job دیگه هم وجود داره که میتونید سرچ کنید و در موردش بخونید: enqueue job، script job، TopLevelModuleEvaluationJob )
هر Job میتونه Jobهای دیگهای رو به انتهای همین صف در حال اجرای Job queue اضافه کنه. تئوریک میتونیم با این قابلیت یه لوپ بینهایت درست کنیم. یه جابی باشه که هی جاب اضافه کنه به انتهای صف در حال اجرا. یه چیزی مشابه while true.
جاب ها مشابه setTimeout با پارامتر دوم 0 هست ولی گارانتی میکنه حتماً بعد از خالی شدن کال استک اولین چیزی باشه که اجرا میشه.
کالبک ها
همونطور که میدونید کالبکها رایجترین روش برای مدیریت فرآیند اسینکرنس و یه پترن خیلی مهم و اساسی توی جاوااسکریپت به حساب میان. خیلی از اپلیکیشنهای ساده و پیچیده بر مبنای همین کالبکها ساخته شدن.
قطعاًً میدونیم که کالبک ضعفهای خودش رو داره و خیلیها تلاش کردن راهحلهای دیگهای برای حل مسأله پیدا کنن. دونستن زیربنای اون راهحلهای جدید، به ما کمک میکنه بهتر استفاده کنیم از اونها.
کالبک های تو در تو
به کد زیر دقت کنید:
listen('click', function (e) {
setTimeout(function() {
ajax('https://api.example.com/endpoint', function (text) {
if (text == "hello") {
doSomething();
} else if (text == "world") {
doSomethingElse();
}
});
}, 500);
});
سه تا تابع تو در تو رو مشاهده میکنید که هر کدوم نمایندهی یکی از قسمتهای اسینکرنس در جاوااسکریپت هستند. listener، setTimeout و ajax.
به این سبک کد میگن callback hell ولی تقریباً میشه گفت کالبک هل ربطی به تابعهای تو در تو نداره و مشکل از جای دیگهای هست که آب میخوره. هروقت دولوپرها به ترتیب اجرای برنامه کدهاشون رو مینویسن کالبک هل به وجود میاد. در ادامه میخوایم در مورد روشهایی صحبت کنیم که ترتیب کدنویسی رو برای ما تغییر میدن.
پایان قسمت اول
مطلبی دیگر از این انتشارات
نوشتن کوئری Mysql در Node.js
مطلبی دیگر از این انتشارات
با آرایه های جاوا اسکریپت مثل یک رئیس رفتار کن!
مطلبی دیگر از این انتشارات
چگونه در node و nginx در حالت توزیع شده از socket-io استفاده کنیم