انتخاب رویکرد مناسب در برنامه نویسی ناهمگام جاوا اسکریپت — راهنمای جامع
در آخرین بخش از این سری مقالات برنامه نویسی ناهمگام جاوا اسکریپت بررسی مختصری در خصوص تکنیکها و قابلیتهای مختلف کدنویسی داریم که در طی این دوره آموزش داده شده است. همچنین بررسی میکنیم که باید از کدام رویکردها استفاده کنیم و برخی توصیهها و یادآوریها در مورد تلههای رایج ارائه شدهاند.
پیشنیاز مطالعه این نوشته داشتن سواد مقدماتی رایانه و درک معقولی از مبانی جاوا اسکریپت است. هدف از این مقاله نیز آشنا ساختن مخاطب با روش تشخیص زمان مناسب استفاده از تکنیکهای مختلف برنامهنویسی ناهمگام است. برای مطالعه بخش قبلی این سری مقالات آموزشی به لینک زیر رجوع کنید:
Callback-های ناهمگام
Callback-ها عموماً در API-های به سبک قدیم مشاهده میشوند که در آنها تابعی به عنوان پارامتر به تابع دیگر ارسال میشود و زمانی که یک عملیات ناهمگام تکمیل شد فراخوانی میشود و callback نیز به نوبه خود کاری روی نتیجه اجرا میکند. callback-ها تا قبل از promise-ها استفاده میشدند و کارایی و انعطاف مورد نیاز را نداشتند. بنابراین تنها در موارد ضرورت باید از آنها استفاده کرد.
استفاده از Callback در موارد زیر مناسب است/نیست:
عملیات با تأخیر منفرد | عملیات مکرر | عملیات ترتیبی چندگانه | عملیات همزمان چندگانه |
---|---|---|---|
خیر | بله (callback-های بازگشتی) | بله (callback-های تو در تو) | خیر |
نمونه کد
در ادامه مثالی را مشاهده میکنید که یک منبع را از طریق API به نام XMLHttpRequest بارگذاری میکند:
1function loadAsset(url, type, callback) {
2 let xhr = new XMLHttpRequest();
3 xhr.open('GET', url);
4 xhr.responseType = type;
5
6 xhr.onload = function() {
7 callback(xhr.response);
8 };
9
10 xhr.send();
11}
12
13function displayImage(blob) {
14 let objectURL = URL.createObjectURL(blob);
15
16 let image = document.createElement('img');
17 image.src = objectURL;
18 document.body.appendChild(image);
19}
20
21loadAsset('coffee.jpg', 'blob', displayImage);
تلهها
- Callback-های تو در تو میتوانند پیچیده باشند و خوانش دشواری پیدا کنند که به نام جهنم callback مشهور است.
- Callback-های ناموفق باید به ازای هر سطح از تودرتو سازی یک بار فراخوانی شوند، در حالی که با استفاده از promise-ها میتوان از یک بلوک ()catch. منفرد برای مدیریت خطاها در کل زنجیره استفاده کرد.
- Callback-های ناهمگام چندان مناسب نیستند.
- Callback-های promise همواره در ترتیب صحیحی که در صف رویداد قرار گرفتهاند فراخوانی میشوند، در حالی که Callback-های ناهمگام چنین نیستند.
سازگاری مرورگر
مرورگرها پشتیبانی نسبتاً خوبی از Callback دارند، گرچه پشتیبانی دقیق از Callback-ها در API-ها به هر API خاص بستگی دارد. برای اطلاع از پشتیبانی هر API باید به مستندات آن مراجعه کنید.
()setTimeout
()setTimeout متدی است که امکان اجرای یک تابع پس از مقدار زمان دلخواه را فراهم میسازد.
()setTimeout برای موارد زیر مناسب است/نیست:
عملیات با تأخیر منفرد | عملیات مکرر | عملیات ترتیبی چندگانه | عملیات همزمان چندگانه |
---|---|---|---|
بله | بله (timeout-های بازگشتی) | بله (timeout-های تو در تو) | خیر |
نمونه کد
در کد زیر مرورگر به مدت دو ثانیه منتظر خواهد ماند تا تابع ناهمگام اجرا شود و سپس پیام هشدار را نمایش میدهد:
1let myGreeting = setTimeout(function() {
2 alert('Hello, Mr. Universe!');
3}, 2000)
تلهها
با کدی مانند زیر میتوان از فراخوانیهای ()setTimeout بازگشتی برای اجرای مکرر یک تابع به روشی مشابه ()setInterval استفاده کرد:
1let i = 1;
2setTimeout(function run() {
3 console.log(i);
4 i++;
5
6 setTimeout(run, 100);
7}, 100);
البته تفاوتی بین ()setTimeout و ()setInterval بازگشتی وجود دارد:
- ()setTimeout بازگشتی تضمین میکند که دستکم مقدار زمان تعیینشده (در این مثال 100 میلیثانیه) بین دو اجرای تابع زمان وجود خواهد داشت، یعنی کد اجرا خواهد شد و سپس تا قبل از اجرای مجدد، 100 میلیثانیه صبر خواهد کرد. بازه زمانی مورد نظر صرف نظر از مدت زمانی که کد برای اجرا صبر میکند همان خواهد بود.
- در زمان استفاده از ()setInterval، بازه مورد نظر که انتخاب میکنیم شامل زمانی خواهد بود که طول میکشد تا کد منتظر اجرا بماند. فرض کنید اجرای کد 40 میلیثانیه طول بکشد، در این صورت بازه انتظار مورد نظر در نهایت 60 میلیثانیه خواهد بود.
زمانی که انتظار میرود کد، زمان اجرایی طولانیتر از بازهی تعیین شده داشته باشد، بهتر است از ()setTimeout بازگشتی استفاده کنیم. بدین ترتیب بازه زمانی ثابتی بین اجراها لحاظ میشود و مهم نیست که اجرای کد جه قدر طول بکشد. بدین ترتیب از بروز خطا اجتناب میشود.
سازگاری مرورگر
()setInterval
()setInterval متدی است که امکان اجرای مکرر تابع را با بازه انتظار تنظیم شده بین هر اجرا فراهم میسازد. ()setInterval به اندازه ()requestAnimationFrame کارآمد نیست، اما امکان انتخاب نرخ فریم. نرخ اجرا را میدهد.
برای موارد زیر مناسب است/نیست:
عملیات با تأخیر منفرد | عملیات مکرر | عملیات ترتیبی چندگانه | عملیات همزمان چندگانه |
---|---|---|---|
خیر | بله | نه (مگر این که یکسان باشد) | خیر |
نمونه کد
تابع زیر یک شیء ()Date ایجاد میکند، رشته زمانی را با استفاده از ()toLocaleTimeString از آن استخراج میکند و سپس آن را در رابط کاربری نمایش میدهد. سپس آن را هر ثانیه یک بار با استفاده از ()setInterval اجرا میکنیم و جلوهای شبیه به یک ساعت دیجیتالی ایجاد میکنیم که هر ثانیه یک بار بهروزرسانی میشود:
1function displayTime() {
2 let date = new Date();
3 let time = date.toLocaleTimeString();
4 document.getElementById('demo').textContent = time;
5}
6
7const createClock = setInterval(displayTime, 1000);
تلهها
نرخ فریم برای سیستمی که انیمیشن روی آن اجرا میشود، بهینهسازی نشده و ممکن است ناکارآمد باشد. به جز در مواردی که نیاز به نرخ فریم پایینتر (آهستهتر) داشته باشیم، عموماً بهتر است از ()requestAnimationFrame استفاده کنیم.
سازگاری مرورگر
()requestAnimationFrame
()requestAnimationFrame متدی است که امکان اجرای مکرر تابع را به روشی کارآمد فراهم میسازد. بهترین نکته در مورد این متد آن است که بهترین نرخ فریم ممکن را برای مرورگر/سیستم جاری به دست میدهد. شما باید در صورت امکان از این متد به جای ()setInterval() / setTimeout بازگشتی استفاده کنید، مگر این که به نرخ فریم خاصی نیاز داشته باشید.
برای موارد زیر مناسب است/ نیست:
عملیات با تأخیر منفرد | عملیات مکرر | عملیات ترتیبی چندگانه | عملیات همزمان چندگانه |
---|---|---|---|
خیر | بله | خیر (مگر این که یکسان باشد) | خیر |
نمونه کد
در مثال زیر یک اسپینر ساده انیمیت شده را میبینید:
1const spinner = document.querySelector('div');
2let rotateCount = 0;
3let startTime = null;
4let rAF;
5
6function draw(timestamp) {
7 if(!startTime) {
8 startTime = timestamp;
9 }
10
11 let rotateCount = (timestamp - startTime) / 3;
12
13 spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
14
15 if(rotateCount > 359) {
16 rotateCount = 0;
17 }
18
19 rAF = requestAnimationFrame(draw);
20}
21
22draw();
تلهها
هنگام استفاده از متد ()requestAnimationFrame امکان انتخاب یک نرخ فریم خاص وجود ندارد. اگر نیاز داشته باشید که انیمیشن با نرخ فریم کُندتری کار کند، باید از ()setInterval یا ()setTimeout بازگشتی استفاده کند.
سازگاری مرورگر
Promise-ها
Promise-ها قابلیتی از جاوا اسکریپت هستند که امکان اجرای عملیات ناهمگام را میدهند و تا زمانی که تابع به طور کامل اجرا نشده است منتظر میمانند تا بر اساس نتیجه آن عملیات دیگر را اجرا کنند. Promise-ها ستون فقرات جاوا اسکریپت مدرن ناهمگام محسوب میشوند.
برای موارد زیر مناسب است / نیست:
عملیات با تأخیر منفرد | عملیات مکرر | عملیات ترتیبی چندگانه | عملیات همزمان چندگانه |
---|---|---|---|
خیر | خیر | بله | به بخش ()Promise.all در ادامه مراجعه کنید. |
نمونه کد
کد زیر یک تصویر را از سرور واکشی کرده و آن را درون یک عنصر <img> نمایش میدهد:
1fetch('coffee.jpg')
2.then(response => response.blob())
3.then(myBlob => {
4 let objectURL = URL.createObjectURL(myBlob);
5 let image = document.createElement('img');
6 image.src = objectURL;
7 document.body.appendChild(image);
8})
9.catch(e => {
10 console.log('There has been a problem with your fetch operation: ' + e.message);
11});
تلهها
زنجیرههای Promise میتوانند پیچیده باشند و تجزیه آنها دشوار باشد. اگر چند Promise را به صورت تو در تو تعریف کنید، ممکن است در نهایت با همان مشکل جهنم callback مواجه شوید. برای مثال به کد زیر توجه کنید:
1remotedb.allDocs({
2 include_docs: true,
3 attachments: true
4}).then(function (result) {
5 var docs = result.rows;
6 docs.forEach(function(element) {
7 localdb.put(element.doc).then(function(response) {
8 alert("Pulled doc with id " + element.doc._id + " and added to local db.");
9 }).catch(function (err) {
10 if (err.name == 'conflict') {
11 localdb.get(element.doc._id).then(function (resp) {
12 localdb.remove(resp._id, resp._rev).then(function (resp) {
13// et cetera...
بهتر است از قدرت زنجیرهسازی Promise-ها برای ایجاد ساختار مسطحتر و با تجزیه آسانتر استفاده کنید:
1remotedb.allDocs(...).then(function (resultOfAllDocs) {
2 return localdb.put(...);
3}).then(function (resultOfPut) {
4 return localdb.get(...);
5}).then(function (resultOfGet) {
6 return localdb.put(...);
7}).catch(function (err) {
8 console.log(err);
9});
یا حتی:
1remotedb.allDocs(...)
2.then(resultOfAllDocs => {
3 return localdb.put(...);
4})
5.then(resultOfPut => {
6 return localdb.get(...);
7})
8.then(resultOfGet => {
9 return localdb.put(...);
10})
11.catch(err => console.log(err));
سازگاری مرورگر
()Promise.all
یکی از قابلیتهای جاوا اسکریپت این است که میتوان منتظر چند Promise ماند تا این Promise-ها به پایان برسند. و یک عملیات دیگر بر مبنای نتایج این Promise-ها اجرا کرد.
برای موارد زیر مناسب است/نیست:
عملیات با تأخیر منفرد | عملیات مکرر | عملیات ترتیبی چندگانه | عملیات همزمان چندگانه |
---|---|---|---|
خیر | خیر | خیر | بله |
نمونه کد
در مثال زیر چند منبع از سرور واکشی میشوند و از ()Promise.all استفاده میشود تا زمانی که همه منابع آماده شدند منتظر بماند و سپس همه آنها را نمایش میدهد:
1function fetchAndDecode(url, type) {
2 // Returning the top level promise, so the result of the entire chain is returned out of the function
3 return fetch(url).then(response => {
4 // Depending on what type of file is being fetched, use the relevant function to decode its contents
5 if(type === 'blob') {
6 return response.blob();
7 } else if(type === 'text') {
8 return response.text();
9 }
10 })
11 .catch(e => {
12 console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message);
13 });
14}
15
16// Call the fetchAndDecode() method to fetch the images and the text, and store their promises in variables
17let coffee = fetchAndDecode('coffee.jpg', 'blob');
18let tea = fetchAndDecode('tea.jpg', 'blob');
19let description = fetchAndDecode('description.txt', 'text');
20
21// Use Promise.all() to run code only when all three function calls have resolved
22Promise.all([coffee, tea, description]).then(values => {
23 console.log(values);
24 // Store each value returned from the promises in separate variables; create object URLs from the blobs
25 let objectURL1 = URL.createObjectURL(values[0]);
26 let objectURL2 = URL.createObjectURL(values[1]);
27 let descText = values[2];
28
29 // Display the images in <img> elements
30 let image1 = document.createElement('img');
31 let image2 = document.createElement('img');
32 image1.src = objectURL1;
33 image2.src = objectURL2;
34 document.body.appendChild(image1);
35 document.body.appendChild(image2);
36
37 // Display the text in a paragraph
38 let para = document.createElement('p');
39 para.textContent = descText;
40 document.body.appendChild(para);
41});
تلهها
اگر یک ()Promise.all رد شود، در این صورت یک یا چند مورد از Promise-هایی که درون پارامترهای آرایه آن وارد شده باید رد شوند، در غیر این صورت Promise-ها کلاً بازگشت نمییابند. بدین ترتیب باید آنها را یک به یک بررسی کنید تا ببینید کدام یک بازگشت یافتهاند.
سازگاری مرورگر
Async/await
Async/await یک ساختار نمادین (Syntactic sugar) است که بر مبنای promise-ها ساخته شده و امکان اجرای عملیات ناهمگام را با استفاده از ساختاری فراهم میکند که بیشتر شبیه نوشتن کد callback همگام است.
برای موارد زیر مناسب است/ نیست:
عملیات با تأخیر منفرد | عملیات مکرر | عملیات ترتیبی چندگانه | عملیات همزمان چندگانه |
---|---|---|---|
خیر | خیر | بله | بله (در ترکیب با ()Promise.all) |
نمونه کد
مثال زیر یک بازنویسی از مثال Promise سادهای است که قبلاً دیدیم و تصاویر را واکشی کرده و نمایش میداد و این بار با استفاده Async/await نوشته شده است:
1async function myFetch() {
2 let response = await fetch('coffee.jpg');
3 let myBlob = await response.blob();
4
5 let objectURL = URL.createObjectURL(myBlob);
6 let image = document.createElement('img');
7 image.src = objectURL;
8 document.body.appendChild(image);
9}
10
11myFetch();
تلهها
- از عملگر await نمیتوان درون یک تابع غیر ناهمگام یا در سطح بالای ساختار کد استفاده کرد. این موضوع در برخی موارد موجب نیاز به ایجاد پوشش تابعی اضافی میشود که در برخی شرایط ممکن است دشوار باشد، اما در اغلب موارد ارزشش را دارد.
- پشتیبانی مرورگر برای async/await به اندازه Promise-ها مناسب است. اگر میخواهید از async/await استفاده کنید، اما در مورد پشتیبانی مرورگرهای قدیمی دغدغه دارید، میتوانید از کتابخانه BabelJS استفاده کنید. این کتابخانه امکان نوشتن اپلیکیشنها را با استفاده از جدیدترین کدهای جاوا اسکریپت میدهد و سپس تغییرات مورد نظر را بسته به نیاز در مورد مرورگرهای کاربر اعمال میکند.
سازگاری مرورگر
بدین ترتیب به پایان این مقاله میرسیم.
برای مطالعه بخش بعدی به این لینک بروید:
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش JavaScript ES6 (جاوا اسکریپت)
- آموزش جاوا اسکریپت — مجموعه مقالات جامع وبلاگ فرادرس
- Promise.all در جاوا اسکریپت — از صفر تا صد
==