تا چند سال پیش، توسعه دهندگان JavaScript معمولا انتخابی جز این که منطق پیچیدهای را فقط با توابع callback انجام دهند، نداشتند.
در واقع، ممکن است که قبلا عبارت «callback hell» را شنیده باشید. این عبارت بی دلیل ساخته نشده بود: کدی که بر پایه callback باشد، به ناچار در جایی ختم میشود که توسعه دهندگان در موقعیتهای مهلک گریه میکنند. و تا زمانی که promiseها به صحنه رسیدند، callbackهای پیچیده برای انجام هر کار پرکاربری با استفاده از JavaScript مورد نیاز بودند.
خوشبختانه، معرفی promiseها همه چیز را حل کرد. Promiseها عقلانیت را به کد ناهمگام پیچیده بر میگردانند. و به همراه سینتکس async / await جدید JavaScript، آنها نیاز به callbackها را به کلی از بین میبرند.
اما فقط یک نکته وجود دارد:
Promiseها callbackها را رام میکنند. آنها کنترل را به شما بر میگردنند، و شما را قادر میسازند تا عملیاتهای IO را به راحتی انجام دهید. اما promiseها همچنان بر پایه callbackها ساخته شدهاند. آنها همچنان ناهمگام هستند. پس برای شروع سفر خود در یادگیری JavaScript از نوع async، بیایید بررسی کنیم که کد ناهمگام اصلا چرا مورد نیاز است.
JavaScript نمیتواند چند کار را به صورت همزمان انجام دهد
وقتی که میخواهید برنامههای واقعی بسازید، باید با دنیای خارج در تعامل باشید. برای مثال، شما شاید بخواهید به ورودی کاربر پاسخ دهید، درخواستهایی را به سرور HTTP ارسال کنید یا منتظر timerها بمانید. به زبانی دیگر، باید عملیاتهای IO انجام دهید.
نکته قابل توجه درباره عملیاتهای IO این است که زمان میبرند؛ همیشه تاخیری میان شروع عملیات و پاسخ آن وجود دارد. و برای JavaScript، این نمایانگر یک مشکل جزئی است.
یکی از مشخصههای تعریفکننده JavaScript، این است که single-thread میباشد. یعنی این که JavaScript نمیتواند اجرای اسکریپتها را قطع کند. حتی اگر اسکریپت مورد نظر فقط منتظر این است که عملیات IO کامل شود، مرورگر نمیتواند تا زمانی که آخرین دستورالعمل موجود در اسکریپت را تکمیل کرده است، کاری انجام دهد. JavaScript حتی نمیتواند رابط کاربری را مجددا ترسیم کند یا این که به ورودی کاربر پاسخ دهد.
برای درک این مسئله، این اسکریپت را در نظر بگیرید که برای کشیدن یک انیمیشن ساده میباشد:
drawKeyFrame(1);
wait(1000);
drawKeyFrame(2);
wait(1000);
drawKeyFrame(0);
در زبانهای multi-thread مانند Python، Ruby یا C#، runtime میتواند همزمان با انتظار اسکریپت در میان keyframeها، کار خود را انجام دهد. Runtime میتواند به ورودی کاربر پاسخ دهد و فریمهای حد واسط انیمیشن را ترسیم کند.
در تضاد، اجرای این اسکریپت در JavaScript باعث توقف مرورگر خواهد شد. برای دیدن این مسئله در عمل، در مثال زیر اول بر روی start و سپس بر روی alert کلیک کنید. صفحه مرورگر به کلی قفل خواهد کرد، تا زمانی که onclick کار خود را به اتمام برساند.
فایل main.js:
function wait(ms) {
let waitUntil = Date.now() + ms
while (Date.now() < waitUntil) { continue }
}
document.querySelector('#start-button').onclick = () => {
let box = document.querySelector('#box')
box.classList.remove('keyframe-1', 'keyframe-2');
wait(1000);
box.classList.add('keyframe-1');
wait(1000);
box.classList.add('keyframe-2');
}
document.querySelector('#alert-button').onclick = () => {
alert('Alert: you clicked "alert!"')
}
فایل styles.css:
div {
position: absolute;
left: 50px;
top: 50px;
height: 50px;
width: 50px;
background-color: red;
opacity: 1;
transform: translate(0, 0);
transition: transform 500ms ease-in-out, opacity 500ms ease-out;
}
div.keyframe-1 {
transform: translate(100px, 0);
}
div.keyframe-2 {
opacity: 0;
}
فایل index.html:
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/babel-regenerator-runtime@6.5.0/runtime.js"></script>
<link rel="stylesheet" type="text/css" href="/styles.css" />
</head>
<body>
<div id="box"></div>
<button id="start-button">START</button>
<button id="alert-button">ALERT</button>
<script type="module" src="main.js"></script>
</body>
</html>
پس JavaScript نمیتواند چند کار را به صورت همزمان انجام دهد؛ اما همچنان باید بتواند که بدون قفل کردن مرورگر، عملیاتهای IO را انجام دهد. و برای ممکن کردن این مسئله، JavaScript از callbackها استفاده میکند.
IO بر پایه Callback
یک callback، فقط یک تابع JavaScript ساده است که میتواند در پاسخ به یک رویداد فراخوانی شود.
وقتی که یک تابع مربوط به IO مانند setTimeout() را فراخوانی میکنید، معمولا یک callback را منتقل میکنید که مرورگر آن را تا زمانی که مورد نیاز باشد، ذخیره میکند. سپس وقتی که رویداد مورد نظر مانند timeout یا پاسخ HTTP پیش میآید، مرورگر میتواند آن را با اجرای تابع callback ذخیره شده مدیریت کند.
نکته مهم در اینجا این است که وقتی شما عملیات IO را شروع میکنید، مرورگر قبل از ادامه دادن، منتظر آن نمیماند تا کامل شود. اسکریپت مورد نظر فقط به اجرا کردن ادامه میدهد. مرورگر فقط بعد از اجرای کامل اسکریپت میتواند callback را اجرا کند و به رویداد پاسخ دهد.
این یعنی کد شما ناهمگام است و خارج از نظم اجرا میشود. شما میتوانید این مسئله را در مثال زیر که آخرین بیانیه console.log() قبل از مورد وسطی اجرا میشود، در عمل ببینید. حتی با وجود این که مورد وسطی برنامهریزی شده است تا اول از همه اجرا شود!
فایل main.js:
function wait(ms) {
let waitUntil = Date.now() + ms
while (Date.now() < waitUntil) { continue }
}
document.querySelector('#start-button').onclick = () => {
console.log('Start!')
setTimeout(() => {
console.log('50 milliseconds have passed!')
}, 50)
wait(100)
console.log('100 milliseconds have passed!')
}
فایل index.html:
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/babel-regenerator-runtime@6.5.0/runtime.js"></script>
</head>
<body>
<div id="box"></div>
<button id="start-button">START</button>
<script type="module" src="main.js"></script>
</body>
</html>
Callback Hell
شما میتوانید با تمرین کردن، به رسیدگی به callbackهای تکی که فقط یک یا دو کار را انجام میدهند، عادت کنید. اما متاسفانه، در دنیای واقعی این مسئله پیچیده است. گاهی اوقات باید callbackها را ترکیب کنید، و در اینجاست که همه چیز به هم ریخته میشود.
برای مثال، فرض کنید که شما در حال bundle کردن یک انیمیشن حلقه با استفاده از setTimeout() و transitionهای CSS هستید. در طی هر keyframe، شما باید مقداری کلاسهای CSS را اضافه کرده، یا حذف کنید و سپس setTimeout() را فراخوانی کنید تا در keyframe بعدی اجرا شود.
این مسئله با فقط سه keyframe، به این صورت خواهد بود:
فایل main.js:
let element = document.getElementById('root')
function animate() {
element.classList.add('keyframe-1');
setTimeout(() => {
element.classList.add('keyframe-2');
setTimeout(() => {
element.classList.remove('keyframe-1', 'keyframe-2');
setTimeout(animate, 1000)
}, 1000);
}, 1000);
}
animate()
فایل styles.css:
div {
position: absolute;
left: 50px;
top: 50px;
height: 50px;
width: 50px;
background-color: red;
opacity: 1;
transform: translate(0, 0);
transition: transform 500ms ease-in-out, opacity 500ms ease-out;
}
div.keyframe-1 {
transform: translate(100px, 0);
}
div.keyframe-2 {
opacity: 0;
}
فایل index.html:
<!DOCTYPE html>
<html>
<head>
<title>Untitled App</title>
<script src="https://unpkg.com/react@16.4.1/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.4.1/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/prop-types@15.6.2/prop-types.js"></script>
<link rel="stylesheet" type="text/css" href="/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="main.js"></script>
</body>
</html>
بد به نظر میرسد، نه؟ اما مقداری را فرض کنید که به جای سه keyframe، شما ده keyframe داشتید. بیانیه setTimeout() تو در تو به مقدار زیادی whitespace ختم خواهد شد که میتوانید با استفاده از آن یک هرم بسازید. این چیزی است که مردم آن را callback hell مینامند. و در کد بر پایه callback، این هرمها در هر جایی که شما باید با هر چیز مهمی در تعامل باشید، ظاهر میشوند. مثلا خواندن و نوشتن فایلها، تعامل با یک سرور و...
خوشبختانه، JavaScript مدرن راهی برای خلاصی از این مسئله به شما میدهد.
Promiseها
Promiseها به همراه سینتکس async / await شما را قادر میسازند تا کدی بنویسید که به ترتیب مورد انتظار شما اجرا میشود، در حالیکه باعث نمیشود مرورگر قفل کند.
برای مثال، به این صورت میتوانید انیمیشین بالا را با استفاده از promiseها، async و await پیادهسازی کنید:
فایل main.js:
function delay(ms) {
return new Promise(resolve => {
window.setTimeout(resolve, ms)
})
}
document.querySelector('#alert-button').onclick = () => {
alert('Alert: you clicked "alert!"')
}
async function animate() {
let box = document.querySelector('#box')
box.classList.add('keyframe-1');
await delay(1000);
box.classList.add('keyframe-2');
await delay(1000);
box.classList.remove('keyframe-1', 'keyframe-2');
setTimeout(animate, 1000)
}
animate()
فایل styles.css:
div {
position: absolute;
left: 50px;
top: 50px;
height: 50px;
width: 50px;
background-color: red;
opacity: 1;
transform: translate(0, 0);
transition: transform 500ms ease-in-out, opacity 500ms ease-out;
}
div.keyframe-1 {
transform: translate(100px, 0);
}
div.keyframe-2 {
opacity: 0;
}
فایل index.html:
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/babel-regenerator-runtime@6.5.0/runtime.js"></script>
<link rel="stylesheet" type="text/css" href="/styles.css" />
</head>
<body>
<div id="box"></div>
<button id="alert-button">ALERT</button>
<script type="module" src="main.js"></script>
</body>
</html>
دقت کردید که تابع animate() چقدر شبیه به مثال اول است؟ تقریبا مانند چیزی به نظر میرسد که شما در یک زبان multi-thread میبینید. Promiseها و async / await به شما منفعتهای کارایی کد ناهمگام را میدهند، بدون این که وضوح مسئله از دست برود.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید