مقدمه
در روزهای اولیه اینترنت، وب سایت ها اغلب از داده های ثابت در یک صفحه HTML تشکیل می شدند. اما اکنون که برنامههای کاربردی وب تعاملیتر و پویاتر شدهاند، انجام عملیات فشرده مانند درخواستهای شبکه خارجی برای بازیابی دادههای API به طور فزایندهای ضروری شده است. برای انجام این عملیات در جاوا اسکریپت، یک توسعه دهنده باید از تکنیک های برنامه نویسی ناهمزمان استفاده کند.
از آنجایی که جاوا اسکریپت یک زبان برنامه نویسی تک رشته ای با یک مدل اجرای همزمان است که عملیات را یکی پس از دیگری پردازش می کند، در هر زمان فقط می تواند یک دستور را پردازش کند. با این حال، اقدامی مانند درخواست داده از یک API بسته به اندازه داده درخواستی، سرعت اتصال به شبکه و سایر عوامل میتواند زمان نامشخصی را ببرد. اگر تماسهای API بهصورت همزمان انجام میشد، مرورگر نمیتواند هیچ ورودی کاربر، مانند پیمایش یا کلیک کردن روی یک دکمه را تا زمانی که آن عملیات کامل شود، مدیریت کند. این به عنوان مسدود کردن شناخته می شود.
به منظور جلوگیری از رفتار مسدود کردن، محیط مرورگر دارای API های وب زیادی است که جاوا اسکریپت می تواند به آنها دسترسی داشته باشد که ناهمزمان هستند، به این معنی که می توانند به جای اجرای متوالی، موازی با سایر عملیات ها اجرا شوند. این مفید است زیرا به کاربر اجازه می دهد تا زمانی که عملیات ناهمزمان در حال پردازش است، به استفاده از مرورگر به طور معمول ادامه دهد.
حلقه رویداد
این بخش توضیح می دهد که چگونه جاوا اسکریپت کدهای ناهمزمان را با حلقه رویداد مدیریت می کند. ابتدا از طریق نمایشی از حلقه رویداد در محل کار اجرا می شود و سپس دو عنصر حلقه رویداد را توضیح می دهد: پشته و صف.
کد جاوا اسکریپتی که از هیچ یک از APIهای وب ناهمزمان استفاده نمیکند، به صورت همزمان اجرا میشود—یک در یک زمان و به صورت متوالی. این با این کد مثال نشان داده می شود که سه تابع را فراخوانی می کند که هر کدام یک عدد را در کنسول چاپ می کنند:
// Define three example functions
function first() {
console.log(1)
}
function second() {
console.log(2)
}
function third() {
console.log(3)
}
در این کد سه تابع تعریف می کنید که با console.log() اعداد را چاپ می کند.
سپس، فراخوانی ها را به توابع بنویسید:
// Execute the functions
first()
second()
third()
خروجی بر اساس ترتیب فراخوانی توابع خواهد بود—first()، second()، سپس three():
Output
1
2
3
هنگامی که از یک Web API ناهمزمان استفاده می شود، قوانین پیچیده تر می شوند. یک API داخلی که میتوانید با آن تست کنید setTimeout است که یک تایمر تنظیم میکند و یک عمل را بعد از مدت زمان مشخص انجام میدهد. setTimeout باید ناهمزمان باشد، در غیر این صورت کل مرورگر در طول انتظار ثابت می ماند که منجر به تجربه کاربری ضعیف می شود.
برای شبیه سازی درخواست ناهمزمان، setTimeout را به تابع دوم اضافه کنید:
// Define three example functions, but one of them contains asynchronous code
function first() {
console.log(1)
}
function second() {
setTimeout(() => {
console.log(2)
}, 0)
}
function third() {
console.log(3)
}
setTimeout دو آرگومان می گیرد: تابعی که به صورت ناهمزمان اجرا می شود و مدت زمانی که قبل از فراخوانی آن تابع منتظر می ماند. در این کد شما console.log را در یک تابع ناشناس قرار دادید و آن را به setTimeout ارسال کردید، سپس تابع را تنظیم کنید تا بعد از 0 میلی ثانیه اجرا شود.
حالا مانند قبل، توابع را فراخوانی کنید:
// Execute the functions
first()
second()
third()
ممکن است انتظار داشته باشید با تنظیم setTimeout روی 0، اجرای این سه عملکرد همچنان منجر به چاپ اعداد به ترتیب متوالی شود. اما از آنجایی که ناهمزمان است، تابع با وقفه در آخرین بار چاپ می شود:
Output
1
3
2
اینکه زمانبندی را روی صفر ثانیه یا پنج دقیقه تنظیم کنید، تفاوتی نمیکند – console.log که با کد ناهمزمان فراخوانی میشود، پس از عملکردهای سطح بالای همزمان اجرا میشود. این به این دلیل اتفاق می افتد که محیط میزبان جاوا اسکریپت، در این مورد مرورگر، از مفهومی به نام حلقه رویداد برای رسیدگی به رویدادهای همزمان یا موازی استفاده می کند. از آنجایی که جاوا اسکریپت می تواند تنها یک دستور را در یک زمان اجرا کند، به حلقه رویداد نیاز دارد تا از زمان اجرای کدام دستور خاص مطلع شود. حلقه رویداد این را با مفاهیم پشته و صف مدیریت می کند.
پشته
پشته یا پشته تماس، وضعیت عملکردی را که در حال حاضر اجرا میشود، نگه میدارد. اگر با مفهوم پشته آشنا نیستید، میتوانید آن را بهعنوان آرایهای با ویژگیهای «Last in, first out» (LIFO) تصور کنید، به این معنی که فقط میتوانید موارد را از انتهای پشته اضافه یا حذف کنید. جاوا اسکریپت فریم فعلی (یا فراخوانی تابع در یک محیط خاص) را در پشته اجرا می کند، سپس آن را حذف می کند و به فریم بعدی می رود.
برای مثالی که فقط حاوی کد همزمان است، مرورگر اجرا را به ترتیب زیر انجام می دهد:
- first() را به پشته اضافه کنید، first() را اجرا کنید که 1 را در کنسول ثبت می کند، first() را از پشته حذف کنید.
- second() را به پشته اضافه کنید، second() را اجرا کنید که 2 را به کنسول وارد می کند، second() را از پشته حذف کنید.
- third () را به پشته اضافه کنید، سوم () را اجرا کنید که 3 را در کنسول ثبت می کند، سوم () را از پشته حذف کنید.
مثال دوم با setTimout به شکل زیر است:
- first() را به پشته اضافه کنید، first() را اجرا کنید که 1 را در کنسول ثبت می کند، first() را از پشته حذف کنید.
- second() را به پشته اضافه کنید، second() را اجرا کنید.
- setTimeout() را به پشته اضافه کنید، setTimeout() API Web را اجرا کنید که تایمر را شروع می کند و تابع ناشناس را به صف اضافه می کند، setTimeout() را از پشته حذف کنید.
- second() را از پشته حذف کنید.
- سوم () را به پشته اضافه کنید، سوم () را اجرا کنید که 3 را در کنسول ثبت می کند، سوم () را از پشته حذف کنید.
- حلقه رویداد صف را برای هر پیام معلق بررسی میکند و تابع ناشناس را از setTimeout() مییابد، تابع را به پشته اضافه میکند که 2 را در کنسول ثبت میکند، سپس آن را از پشته حذف میکند.
با استفاده از setTimeout، یک Web API ناهمزمان، مفهوم صف را معرفی می کند که این آموزش در ادامه به آن می پردازد.
صف
صف که به آن صف پیام یا صف وظیفه نیز گفته می شود، یک منطقه انتظار برای توابع است. هر زمان که پشته تماس خالی باشد، حلقه رویداد صف را برای هرگونه پیام در انتظار، از قدیمی ترین پیام، بررسی می کند. هنگامی که یکی را پیدا کرد، آن را به پشته اضافه می کند، که تابع موجود در پیام را اجرا می کند.
در مثال setTimeout، تابع ناشناس بلافاصله پس از بقیه اجرای سطح بالا اجرا می شود، زیرا تایمر روی 0 ثانیه تنظیم شده بود. مهم است که به خاطر داشته باشید که تایمر به این معنی نیست که کد دقیقاً در 0 ثانیه یا هر زمان مشخصی اجرا می شود، بلکه به این معنی است که تابع ناشناس را در این مدت زمان به صف اضافه می کند. این سیستم صف به این دلیل وجود دارد که اگر تایمر بخواهد تابع ناشناس را مستقیماً به پشته پس از اتمام تایمر اضافه کند، عملکردی را که در حال حاضر در حال اجرا است قطع میکند، که میتواند اثرات ناخواسته و غیرقابل پیشبینی داشته باشد.
نکته: همچنین یک صف دیگر به نام صف شغل یا صف میکروتسک وجود دارد که به وعده ها رسیدگی می کند. وظایف خرد مانند وعدهها با اولویت بالاتری نسبت به وظایف کلان مانند 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 میدهید.
با اجرای این کد موارد زیر به دست می آید:
Output
Just a function
بیایید با setTimeout به عملکردهای اول، دوم و سوم برگردیم. این چیزی است که تا کنون دارید:
function first() {
console.log(1)
}
function second() {
setTimeout(() => {
console.log(2)
}, 0)
}
function third() {
console.log(3)
}
وظیفه این است که تابع سوم را دریافت کنیم تا همیشه اجرا را تا زمانی که عمل ناهمزمان در تابع دوم تکمیل شود به تاخیر بیاندازد. در اینجاست که callback ها وارد می شوند. به جای اجرای اول، دوم و سوم در سطح بالای اجرا، تابع سوم را به عنوان آرگومان به دوم منتقل می کنید. تابع دوم پس از اتمام عمل ناهمزمان، پاسخ تماس را اجرا می کند.
در اینجا سه عملکرد با یک پاسخ تماس اعمال شده است:
// Define three functions
function first() {
console.log(1)
}
function second(callback) {
setTimeout(() => {
console.log(2)
// Execute the callback function
callback()
}, 0)
}
function third() {
console.log(3)
}
حالا اول و دوم را اجرا کنید، سپس سوم را به عنوان آرگومان به دوم منتقل کنید:
first()
second(third)
پس از اجرای این بلوک کد، خروجی زیر را دریافت خواهید کرد:
Output
1
2
3
اول 1 چاپ می شود و پس از اتمام تایمر (در این مورد صفر ثانیه است، اما می توانید آن را به هر مقدار تغییر دهید) 2 و سپس 3 چاپ می شود. تا زمانی که Web API ناهمزمان (setTimeout) کامل شود، کار کنید.
نکته کلیدی در اینجا این است که توابع پاسخ به تماس ناهمزمان نیستند – setTimeout یک Web API ناهمزمان است که مسئول رسیدگی به وظایف ناهمزمان است. پاسخ به تماس فقط به شما این امکان را می دهد که از زمانی که یک کار ناهمزمان کامل شده است و موفقیت یا شکست آن کار را مدیریت می کند مطلع شوید.
اکنون که نحوه استفاده از callbacks را برای انجام وظایف ناهمزمان یاد گرفتهاید، بخش بعدی مشکلات مربوط به تودرتو کردن تعداد بیش از حد تماسها و ایجاد یک “هرم عذاب” را توضیح میدهد.
Callback تو در تو و هرم عذاب
توابع برگشت به فراخوان روشی مؤثر برای اطمینان از اجرای تاخیری یک تابع تا زمانی که تابع دیگری کامل شده و با داده برگردد، می باشد. با این حال، به دلیل ماهیت تودرتوی تماسهای برگشتی، اگر درخواستهای ناهمزمان متوالی زیادی داشته باشید که به یکدیگر متکی هستند، کد میتواند بههم ریخته شود. این یک ناامیدی بزرگ برای توسعه دهندگان جاوا اسکریپت در اوایل بود، و در نتیجه کدهای حاوی تماس های تودرتو اغلب “هرم عذاب” یا “جهنم پاسخ به تماس” نامیده می شوند.
در اینجا نمایشی از Callback های تو در تو وجود دارد:
function pyramidOfDoom() {
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
}, 500)
}, 2000)
}, 1000)
}
در این کد، هر setTimeout جدید در داخل یک تابع مرتبه بالاتر قرار می گیرد و شکل هرمی از تماس های عمیق تر و عمیق تر ایجاد می کند. اجرای این کد موارد زیر را به همراه خواهد داشت:
Output
1
2
3
در عمل، با کدهای ناهمزمان دنیای واقعی، این می تواند بسیار پیچیده تر شود. به احتمال زیاد نیاز دارید که خطاها را در کدهای ناهمزمان مدیریت کنید و سپس مقداری داده از هر پاسخ را به درخواست بعدی ارسال کنید. انجام این کار با callback ها، خواندن و نگهداری کد شما را دشوار می کند.
در اینجا یک مثال قابل اجرا از یک “هرم عذاب” واقعی تر است که می توانید با آن بازی کنید:
// Example asynchronous function
function asynchronousRequest(args, callback) {
// Throw an error if no arguments are passed
if (!args) {
return callback(new Error('Whoa! Something went wrong.'))
} else {
return setTimeout(
// Just adding in a random number so it seems like the contrived asynchronous function
// returned different data
() => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
500,
)
}
}
// Nested asynchronous requests
function callbackHell() {
asynchronousRequest('First', function first(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest('Second', function second(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest(null, function third(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
})
})
})
}
// Execute
callbackHell()
در این کد، شما باید هر تابع را برای یک پاسخ احتمالی و یک خطای احتمالی حساب کنید، که باعث می شود تابع callbackHell از نظر بصری گیج کننده باشد.
با اجرای این کد موارد زیر به شما داده می شود:
Output
First 9
Second 3
Error: Whoa! Something went wrong.
at asynchronousRequest (<anonymous>:4:21)
at second (<anonymous>:29:7)
at <anonymous>:9:13
نتیجه
این روش مدیریت کد ناهمزمان دشوار است. در نتیجه مفهوم وعده ها در ES6 معرفی شد. این تمرکز بخش بعدی است.