Timeout و Interval در جاوا اسکریپت ناهمگام – راهنمای کاربردی


در این مقاله به بررسی متدهای کلاسیک جاوا اسکریپت از قبیل Timeout و Interval که برای اجرای کد ناهمگام پس از گذشت زمان معین یا در بازههای مشخصی از زمان کاربرد دارند اشاره میکنیم؛ همچنین در مورد فایده آنها صحبت میکنیم و مشکلات ذاتیشان را مورد بررسی قرار میدهیم. برای مطالعه بخش قبلی این سری مقالات آموزشی به لینک زیر بروید:
پیشنیازها
- سواد مقدماتی رایانه
- درک معقولی از مبانی جاوا اسکریپت
هدف از مطالعه این مقاله درک حلقهها و بازههای ناهمگام و موارد کاربرد آنها است.
مقدمه
برای مدتی طولانی، پلتفرم وب چند تابع را در اختیار برنامه نویسان وب قرار داده بود که امکان اجرای ناهمگام کد را پس از گذشت مدت زمان معینی فراهم میساختند. همچنین با این تابعها میتوانستید به اجرای مکرر بلوک کد به صورت ناهمگام تا زمانی که دستور توقف داده نشده بود بپردازید. آنها شامل موارد زیر هستند:
- ()setTimeout: این تابع یک بلوک کد را پس از گذشت مدت زمان معینی اجرا میکند.
- ()setInterval: این تابع یک بلوک کد را به طور مکرر و با تأخیر زمانی ثابتی بین هر فراخوانی اجرا میکند.
- ()requestAnimationFrame: نسخه مدرن ()setInterval است و بلوک کد معینی را پیش از آن که مرورگر بار دیگر ظاهر خود را بازترسیم کند اجرا میکند. بدین ترتیب یک انیمیشن را میتوان با نرخ فریم مناسب صرفنظر از محیطی که در آن اجرا میشود به دست آورد.
کد ناهمگام در عمل از سوی این تابعها روی نخ اصلی اجرا میشود، اما میتوان کد را بین تکرارها نیز اجرا کرد تا درجه کارایی کمتر یا بیشتری داشته باشد و همه اینها به میزان سنگین بودن این کارها از نظر محاسبات پردازنده وابسته هستند. در هر حال، این تابعها برای اجرای مداوم انیمیشنها و دیگر پردازشهای پسزمینه روی یک وبسایت یا وب اپلیکیشن مورد استفاده قرار میگیرند. در بخشهای بعدی روش استفاده از آنها را نمایش خواهیم داد.
()setTimeout
چنان که قبلاً گفتیم، ()setTimeout یک بلوک کد را پس از گذشت مدت زمان معینی اجرا میکند. پارامترهای این تابع به صورت زیر هستند:
- یک تابع که باید اجرا شود و یا یک ارجاع به تابعی که در جای دیگری تعریف شده است.
- یک عدد که نشاندهنده بازه زمانی بر حسب میلیثانیه (بدین ترتیب 1000 به معنی یک ثانیه است) است که باید پیش از اجرای کد منتظر ماند. اگر مقدار 0 ارسال شود (یا کلاً این مقدار نادیده گرفته شود) تابع بیدرنگ اجرا خواهد شد. دلیل این که چرا باید چنین چیزی را خواست، در ادامه بیشتر توضیح میدهیم.
- در ادامه میتوان هیچ پارامتری نداشت یا به چند پارامتر دیگر اشاره کرد که قصد داریم در زمان اجرا به تابع ارسال کنیم.
نکته: از آنجا که timeout callback-ها با همکاری متقابل با یکدیگر اجرا میشوند، هیچ تضمینی وجود ندارد که پس از دقیقاً مقدار معینی از زمان اجرا شوند. اما میتوان مطمئن بود که پس از دست کم زمان تعیین شده اجرا خواهند شد. دستگیرههای timeout نمیتوانند تا زمانی که نخ به نقطهای در اجرای خود نرسیده است که از این دستگیرهها برای یافتن موارد مورد نیاز برای اجرا استفاده کند، قابل اجرا نخواهند بود.
در مثال زیر، مرورگر پیش از اجرای تابع بینام دو ثانیه منتظر میماند، و سپس پیام هشدار را نمایش میدهد:
1<!DOCTYPE html>
2<html lang="en-US">
3 <head>
4 <meta charset="utf-8">
5 <title>Person greeter app</title>
6 </head>
7 <body>
8 <script>
9 let myGreeting = setTimeout(function() {
10 alert('Hello, Mr. Universe!');
11 }, 2000)
12 </script>
13 </body>
14</html>
تابعهایی که استفاده میکنیم لزوماً نباید بینام باشند. میتوان یک تابع دارای نام نیز ارائه کرد و حتی میتوان این تابع را در جای دیگری تعریف کرد و ارجاع تابع را به ()setTimeout ارسال کرد. در ادامه دو نسخه از قطعه کد معادل کد اول را میبینید:
1// With a named function
2let myGreeting = setTimeout(function sayHi() {
3 alert('Hello, Mr. Universe!');
4}, 2000)
5
6// With a function defined separately
7function sayHi() {
8 alert('Hello Mr. Universe!');
9}
10
11let myGreeting = setTimeout(sayHi, 2000);
این وضعیت به طور مثال در صورتی که تابعی داشته باشیم که لازم است از یک timeout و همچنین از یک پاسخ به رویداد قابل فراخوانی باشد، مفید خواهد بود. اما این وضعیت به خصوص اگر timeout callback از چند خط کد تشکیل یافته باشد، میتواند به انسجام کد نیز کمک کند.
()setTimeout یک مقدار شناسه بازگشت میدهد که میتوان از آن در ادامه، مثلاً زمانی که میخواهیم آن را متوقف کنیم، برای اشاره به Timeout استفاده کرد. برای آشنایی با روش انجام این کار به بخش «پاکسازی timeout-ها» در ادامه مراجعه کنید.
ارسال پارامترها به تابع ()setTimeout
هر پارامتری که بخواهیم به تابعی که درون ()setTimeout اجرا میشود، ارسال کنیم باید به صورت پارامترهای اضافی در انتهای لیست پارامترهای ()setTimeout، آنها را مورد اشاره قرار دهیم. برای نمونه میتوانیم تابع قبلی را طوری بازنویسی کنیم که در صورت ارسال نام هر فرد به تابع به او سلام کند:
1function sayHi(who) {
2 alert('Hello ' + who + '!');
3}
نام فردی که قرار است به آن سلام کند، درون فراخوانی ()setTimeout به صورت پارامتر سوم ارسال میشود:
1let myGreeting = setTimeout(sayHi، 2000، 'Mr. Universe');
پاکسازی timeout-ها
در نهایت اگر یک timeout ایجاد شده باشد، میتوان آن را پیش از گذشت زمان معینی با فراخوانی ()clearTimeout لغو کرد. این کار از طریق ارسال شناسه فراخوانی ()setTimeout به صورت یک پارامتر میسر است. بنابراین برای لغو timeout فوق باید کارهای زیر را انجام دهیم:
1clearTimeout(myGreeting);
نکته: در مثال زیر یک دموی نسبتاً پیچیدهتر را مشاهده میکنید که امکان میدهد نام فردی که قرار است به وی سلام شود را در یک فرم تعیین کنیم و با استفاده از دکمه جداگانهای میتوان خوشامدگویی را لغو کرد.
1<!DOCTYPE html>
2<html lang="en-US">
3 <head>
4 <meta charset="utf-8">
5 <title>Person greeter app</title>
6 <style>
7 form {
8 width: 300px;
9 }
10 div {
11 display: flex;
12 justify-content: space-between;
13 margin-bottom: 10px;
14 }
15 button {
16 width: 120px;
17 }
18 </style>
19 </head>
20 <body>
21 <h1>Give someone a greeting!</h1>
22 <form>
23 <div>
24 <label for="name">Enter a name:</label>
25 <input type="name" id="name" required>
26 </div>
27 <div>
28 <button class="greet">Greet!</button>
29 </div>
30 </form>
31
32 <p><button class="cancel">Cancel greeting</button></p>
33 <script>
34 // Store references to our form, input element, and buttons
35 const form = document.querySelector('form');
36 const nameInput = document.getElementById('name');
37 const greetBtn = document.querySelector('.greet');
38 const cancelBtn = document.querySelector('.cancel');
39 // Disable the cancel button for now so it can't do anything if
40 // a greeting is not already in progress
41 cancelBtn.disabled = true;
42 // Define our greeting function
43 function sayHi(who) {
44 alert('Hello ' + who + '!');
45 // Disable the cancel button one greeting has been shown
46 cancelBtn.disabled = true;
47 }
48 // Create a global variable that will act as our setTimeout reference
49 let myGreeting;
50 // Add event listener to our form to start the greeting
51 form.addEventListener('submit', (e) => {
52 // Prevent form submission; we don't want this
53 e.preventDefault();
54 // Enable the cancel button
55 cancelBtn.disabled = false;
56 // set the timeout to greet the name entered in the input
57 myGreeting = setTimeout(sayHi, 5000, nameInput.value);
58 });
59 // Add event listener to the cancel button to cancel the greeting
60 cancelBtn.addEventListener('click', () => {
61 clearTimeout(myGreeting);
62 // Disable the cancel button again
63 cancelBtn.disabled = true;
64 console.log('Greeting cancelled!');
65 });
66 </script>
67 </body>
68</html>
()setInterval
()setTimeout در مواردی که لازم باشد کد را یک بار و پس از مدت معینی اجرا کنیم به خوبی کار میکند. اما وقتی بخواهیم کد را بارها و بارها برای نمونه به صورت یک انیمیشن اجرا کنیم کارایی ندارد.
در این موارد باید از ()setInterval استفاده کنیم. روش کار آن مشابه ()setTimeout است، به جز این که تابعی که به صورت پارامتر اول به آن ارسال میشود به صورت مکرر اجرا میشود و زمان بین هر اجرا بر اساس مقداری که بر حسب میلیثانیه در پارامتر دوم اشاره شده تعیین میشود. همچنین میتوان هر پارامتری که قرار است از سوی تابع، متعاقباً استفاده شود را در پارامترهای بعدی فراخوانی ()setInterval ارسال کرد.
به مثال زیر توجه کنید. تابع زیر یک شیء ()Date ایجاد میکند، رشته زمانی آن را با استفاده از ()toLocaleTimeString استخراج میکند و سپس آن را در UI نمایش میدهد. سپس میتوانیم تابع را هر ثانیه یک بار با استفاده از ()setInterval اجرا کنیم و یک جلوه ساعت دیجیتال بسازیم که هر ثانیه یک بار بهروز میشود. کارکرد کد زیر را میتوانید در این صفحه (+) مشاهده کنید:
1<!DOCTYPE html>
2<html lang="en-US">
3 <head>
4 <meta charset="utf-8">
5 <title>Simple setInterval clock</title>
6 <style>
7 p {
8 font-family: sans-serif;
9 }
10 </style>
11 </head>
12 <body>
13 <p class="clock"></p>
14 <script>
15 function displayTime() {
16 let date = new Date();
17 let time = date.toLocaleTimeString();
18 document.querySelector('.clock').textContent = time;
19 }
20 displayTime();
21 const createClock = setInterval(displayTime, 1000);
22 </script>
23 </body>
24</html>
()setInterval نیز دقیقاً همانند ()setTimeout یک مقدار شناسه بازگشت میدهد که میتوانید متعاقباً از آن برای پاکسازی intervals استفاده کنید.
پاکسازی Intervals
()setInterval یک وظیفه را برای همیشه اجرا میکند، مگر این که کار دیگری در مورد آن انجام دهیم. ممکن است بخواهیم چنین وظایفی را متوقف کنیم، چون در غیر این صورت ممکن است هنگامی که مرورگرها نتوانند نسخههای دیگری از وظیفه را تکمیل کنند با خطا مواجه شویم یا اگر انیمیشنی که از سوی وظیفه مدیریت میشود به پایان برسد باید آن را متوقف کنیم. این کار به همان روشی که timeouts را متوقف کردیم، اجرا میشود. به این منظور باید شناسه بازگشتی از فراخوانی ()setInterval را به تابع ()clearInterval ارسال کنید:
1const myInterval = setInterval(myFunction، 2000);
2
3clearInterval(myInterval);
مثال کاربردی: ایجاد یک کرنومتر
با توجه به همه مطالبی که گفته شد اینک یک چالش برای شما داریم. ابتدا کد زیر را در فایلی به نام setInterval-clock.html روی سیستم خود کپی کنید:
1<!DOCTYPE html>
2<html lang="en-US">
3 <head>
4 <meta charset="utf-8">
5 <title>Simple setInterval clock</title>
6 <style>
7 p {
8 font-family: sans-serif;
9 }
10 </style>
11 </head>
12 <body>
13 <p class="clock"></p>
14 <script>
15 function displayTime() {
16 let date = new Date();
17 let time = date.toLocaleTimeString();
18 document.querySelector('.clock').textContent = time;
19 }
20 displayTime();
21 const createClock = setInterval(displayTime, 1000);
22 </script>
23 </body>
24</html>
سپس آن را با توجه به مواردی که در ادامه آمده است طوری تغییر دهید که یک کرنومتر برای خودتان بسازید. شما باید زمان را مانند قبل نمایش دهید، اما در این مثال به موارد زیر نیاز داریم:
- یک دکمه شروع برای آغاز به کار کرنومتر
- یک دکمه توقف برای ایجاد مکث/توقف در اجرای کرنومتر
- یک دکمه ریست برای ریست کردن زمان به مقدار صفر.
- زمان نمایش یافته به جای زمان واقعی، تعداد ثانیههای گذشته را ثبت میکند.
در ادامه برای اجرای وظایف فوق چند سرنخ در اختیار شما قرار میدهیم:
شما میتوانید نشانهگذاری دکمه را بر حسب سلیقه خود استایلدهی کنید، فقط باید مطمئن شوید که از HTML معناشناختی استفاده میکنید که امکان دریافت ارجاع دکمه را در جاوا اسکریپت فراهم میسازد. احتمالاً بخواهید یک متغیر بسازید که از 0 آغاز شود و سپس آن را هر ثانیه یک بار با استفاده از یک حلقه ثابت افزایش دهید.
ایجاد مثال بدون استفاده از شیء ()Date چنان که در نسخه قبلی اجرا کردیم، آسانتر است، اما دقت کمتری دارد و نمیتوان مطمئن بود که callback دقیقاً پس از 1000 میلیثانیه اجرا میشود. روش دقیقتر میتواند اجرای کد ()startTime = Date.now برای دریافت زمان جاری دقیقاً از زمان کلیک کردن دکمه شروع از سوی کاربر باشد و سپس میتوان از Date.now() – startTime برای دریافت تعداد میلیثانیههای سپری شده از زمان کلیک کردن دکمه استفاده کرد.
همچنین میتوانید تعداد ساعتها، دقایق، ثانیهها را به صورت مقادیر متفاوت محاسبه کنید و سپس آنها را با هم در یک رشته پس از هر تکرار حلقه نمایش دهید. در شمارنده دوم میتوان هر کدام از این موارد را محاسبه کرد.
شیوه محاسبه آنها چگونه است؟ به موارد زیر فکر کنید:
- تعداد ثانیههای موجود در یک ساعت برابر با 3600 است.
- تعداد دقایق برابر با مقدار ثانیههای باقیمانده پس از حذف ساعتها و تقسیم کردن بر 60 خواهد بود.
- تعداد ثانیهها مقدار ثانیههایی است که پس از حذف همه دقایق باقی میمانند.
اگر میخواهید ساعت کرنومترتان شبیه کرنومترهای کلاسیک باشد، میتوانید یک صفر ابتدایی برای نمایش مقادیر کمتر از 10 به ابتدای ارقام مربوطه اضافه کنید.
برای ایجاد مکث در کار کرنومتر باید interval را پاک کنید. برای ریست کردن آن باید شمارنده را مجدداً به 0 تنظیم کنید و بیدرنگ نمایش را بهروزرسانی کنید. احتمالاً مجبور باشید دکمه Start را پس از یک بار زدن آن غیرفعال کنید و آن را مجدداً در زمان متوقف شدن کار کرنومتر فعال کنید. در غیر این صورت ممکن است چند نسخه از دکمه استارت به کار افتند و چند نسخه از ()setInterval وجود داشته باشند که منجر به رفتار نادرست میشود.
نکته: اگر در نوشتن کد فوق به مشکل برخورد کردید، میتوانید از سورس کد کامل زیر بهره بگیرید:
1<!DOCTYPE html>
2<html lang="en-US">
3 <head>
4 <meta charset="utf-8">
5 <title>setInterval stopwatch</title>
6 <style>
7 p {
8 font-family: sans-serif;
9 }
10 </style>
11 </head>
12 <body>
13 <p class="clock"></p>
14 <p><button class="start">Start</button><button class="stop">Stop</button><button class="reset">Reset</button></p>
15 <script>
16 // Define a counter variable for the number of seconds and set it to zero.
17 let secondCount = 0;
18 // Define a global to store the interval when it is active.
19 let stopWatch;
20 // Store a reference to the display paragraph in a variable
21 const displayPara = document.querySelector('.clock');
22 // Function to calculate the current hours, minutes, and seconds, and display the count
23 function displayCount() {
24 // Calculate current hours, minutes, and seconds
25 let hours = Math.floor(secondCount/3600);
26 let minutes = Math.floor((secondCount % 3600)/60);
27 let seconds = Math.floor(secondCount % 60)
28 // Display a leading zero if the values are less than ten
29 let displayHours = (hours < 10) ? '0' + hours : hours;
30 let displayMinutes = (minutes < 10) ? '0' + minutes : minutes;
31 let displaySeconds = (seconds < 10) ? '0' + seconds : seconds;
32 // Write the current stopwatch display time into the display paragraph
33 displayPara.textContent = displayHours + ':' + displayMinutes + ':' + displaySeconds;
34 // Increment the second counter by one
35 secondCount++;
36 }
37 // Store references to the buttons in constants
38 const startBtn = document.querySelector('.start');
39 const stopBtn = document.querySelector('.stop');
40 const resetBtn = document.querySelector('.reset');
41 // When the start button is pressed, start running displayCount() once per second using displayInterval()
42 startBtn.addEventListener('click', () => {
43 stopWatch = setInterval(displayCount, 1000);
44 startBtn.disabled = true;
45 });
46 // When the stop button is pressed, clear the interval to stop the count.
47 stopBtn.addEventListener('click', () => {
48 clearInterval(stopWatch);
49 startBtn.disabled = false;
50 });
51 // When the reset button is pressed, set the counter back to zero, then immediately update the display
52 resetBtn.addEventListener('click', () => {
53 secondCount = 0;
54 displayCount();
55 });
56 // Run displayCount() once as soon as the page loads so the clock is displayed
57 displayCount();
58 </script>
59 </body>
60</html>
نکات مهم در مورد ()setTimeout و ()setInterval
چند نکته وجود دارند که هنگام کار کردن با ()setTimeout و ()setInterval باید به خاطر بسپارید. این موارد را در ادامه معرفی میکنیم.
Timeout-های بازگشتی
روش دیگری نیز برای استفاده از ()setTimeout وجود دارد. میتوان آن را به صورت بازگشتی اجرا کرد تا کد یکسانی را به صورت مکرر اجرا کند. در این روش دیگر نیازی به استفاده از ()setInterval نیست.
در مثال زیر از یک ()setTimeout بازگشتی برای اجرای تابع ارسالی در هر 100 میلیثانیه یک بار استفاده شده است:
1let i = 1;
2
3setTimeout(function run() {
4 console.log(i);
5 i++;
6 setTimeout(run, 100);
7}, 100);
مثال فوق را با مثال زیر مقایسه کنید که در آن از ()setInterval برای اجرای تأثیر مشابه استفاده شده است:
1let i = 1;
2
3setInterval(function run() {
4 console.log(i);
5 i++
6}, 100);
تفاوت ()setTimeout بازگشتی و ()setInterval چیست؟
تفاوت بین دو نسخه از کد فوق جزئی است:
- ()setTimeout بازگشتی تضمین میکند تأخیر یکسانی بین اجراهای مختلف وجود دارد. برای نمونه در کد فوق این تأخیر همواره 100 میلیثانیه خواهد بود. کد اجرا میشود و سپس 100 میلیثانیه صبر میکند تا مجدداً اجرا شود، بنابراین بازه زمانی صرفنظر از این که کد چه مقدار اجرا شود یکسان خواهد بود.
- مثال فوق با استفاده از ()setInterval کارها را به ترتیب متفاوتی اجرا میکند. بازهای که ما انتخاب کردهایم شامل زمانی است که برای اجرای کد صرف میشود. یعنی اگر این کد برای اجرا به 40 ثانیه زمان نیاز داشته باشد، بازه مزبور در طی 60 ثانیه به پایان میرسد.
- زمانی که به صورت بازگشتی از ()setTimeout استفاده میکنید، در هر تکرار ممکن است تأخیر متفاوتی پیش از اجرای بعدی محاسبه شود. به بیان دیگر مقدار پارامتر ثانیه را میتوان زمان متفاوتی بر حسب میلیثانیه تعیین کرد تا پیش از اجرای مجدد کد صبر کند.
هنگامی که احتمال میرود اجرای کد زمانی بیش از بازه زمانی تعیین شده طول بکشد، بهتر است از ()setTimeout بازگشتی استفاده شود. بدین ترتیب بازه زمانی بین اجراها صرفنظر از زمانی که برای اجرای کد صرف میشود، همواره ثابت خواهد بود و از بروز خطا جلوگیری میکند.
Timeout-های بیدرنگ
وقتی از مقدار 0 برای ()setTimeout استفاده میکنید، اجرای تابع callback در اولین فرصت ممکن زمانبندی میشود، اما این کار پس از آن که نخ کد اصلی اجرا شد انجام خواهد یافت.
برای نمونه کد زیر یک پیام با مضمون «hello» در خروجی ارائه میکند و سپس زمانی که روی دکمه OK آن کلیک کنید، بیدرنگ یک پیام با مضمون «World» ارائه میکند.
1setTimeout(function() {
2 alert('World');
3}, 0);
4
5alert('Hello');
این وضعیت در مواردی که میخواهید یک بلوک کد به محض پایان یافتن همه کارهای نخ اصلی اجرا شود، مفید خواهد بود. آن را در یک حلقه رویداد async قرار دهید تا پس از آن به صورت سرراستی اجرا شود.
پاکسازی با ()clearTimeout یا ()clearInterval
()clearTimeout و ()clearInterval هر دو از فهرست یکسانی از مداخل برای پاکسازی فرم استفاده میکنند. نکته جالبتر این است که میتوانید از هر کدام از این متدها برای پاکسازی ()clearTimeout یا ()clearInterval استفاده کنید.
برای انسجام کد میتوان از مدخلهای ()clearTimeout برای پاکسازی ()setTimeout و از مدخلهای ()clearInterval برای پاکسازی ()setInterval استفاده کرد. بدین ترتیب از ایجاد سردرگمی اجتناب میشود.
()requestAnimationFrame
تابع ()requestAnimationFrame یک تابع حلقه تخصصی برای ایجاد انیمیشنهای اجرایی به روشی مؤثر در مرورگر محسوب میشود. این تابع اساساً نسخه مدرنتری از ()setInterval است و یک بلوک کد خاص را پیش از آن که مرورگر، صفحه نمایش را مجدداً ترسیم کند اجرا میکند. بدین ترتیب میتوان یک انیمیشن را با نرخ فریم مناسبی رندر کرد و مهم نیست که محیطی که اجرا میشود کدام است.
این تابع در پاسخ به مشکلات رایج ()setInterval طراحی شده است. برای نمونه ()setInterval در نرخ فریم بهینه برای همه دستگاهها اجرا نمیشود و در برخی موارد با فقدان فریم مواجه میشویم. همچنین این تابع در صورتی که برگه مرورگر فعال نباشد یا انیمیشن به خارج از حوزه دید صفحه اسکرول شود، به اجرای خود ادامه میدهد.
متد ()requestAnimationFrame پیش از ترسیم مجدد، یک Callback به عنوان آرگومان میگیرد. این الگوی عمومی است که در زمان استفاده مشاهده میکنید:
1function ()draw {
2 // Drawing code goes here
3 requestAnimationFrame(draw);
4}
5
6()draw;
ایده اصلی این است که یک تابع تعریف کنیم که انیمیشن درون آن بهروزرسانی شود یعنی تصاویر sprite جابجا شوند، امتیاز بهروزرسانی شود، دادهها رفرش شوند و یا تغییراتی از این دست صورت گیرد. سپس آن را در زمان آغاز به کار پردازش فراخوانی کنیم. در انتهای بلوک تابع ()requestAnimationFrame را با ارجاع تابع که به صورت پارامتر ارسال میشود فراخوانی میکنیم و بدین ترتیب به مرورگر اعلام میکنیم که تابع را در زمان ترسیم مجدد صفحه نمایش مجدداً فراخوانی کند. به این ترتیب تابع به صورت مکرر اجرا میشود گویی به صورت بازگشتی ()requestAnimationFrame را فراخوانی کردهایم.
نکته: اگر میخواهید برخی انیمیشنهای ساده و ثابت DOM اجرا کنید، احتمالاً انیمیشنهای CSS روش سریعتری محسوب میشوند، زیرا مستقیماً از سوی کد درونی مرورگر و نه جاوا اسکریپت محاسبه میشوند. با این حال اگر کاری پیچیدهتر اجرا میکنید که نیازمند کار با اشیایی مانند 2D Canvas API یا WebGL هستید که مستقیماً درون DOM در دسترس نیستند، در اغلب موارد استفاده از ()requestAnimationFrame گزینه بهتری محسوب میشود.
سرعت اجرای انیمیشن چقدر است؟
روان بودن انیمیشن به طور مستقیم به نرخ فریم انیمیشن وابسته است و بر اساس تعداد فریم بر ثانیه (fps) اندازهگیری میشود. هر چه این عدد بالاتر باشد، انیمیشن روانتر به نظر میرسد.
از آنجا که اغلب صفحههای نمایش، نرخ رفرش 60 هرتز دارند، سریعترین نرخ فریم که میتوانید در زمان کار با مرورگرهای وب داشته باشید 60 فریم بر ثانیه است. با این حال وجود فریمهای بیشتر به معنی پردازش بیشتر است که میتواند موجب پرش و یا از دست رفتن فریمها شود.
اگر نمایشگری با نرخ رفرش 60 هرتز دارید و میخواهید نرم فریم انیمیشنتان 60 FPS باشد باید هر 16.7 میلیثانیه کد انیمیشن را اجرا کنید تا هر فریم رندر شود. این یک یادآوری است که بدانیم حجم کدی که در هر تکرار از حلقه انیمیشن اجرا میشود، باید تا حد امکان کوچک حفظ شود.
()requestAnimationFrame همواره تلاش میکند که تا حد امکان به نرخ فریم 60 fps نزدیک شوید، البته برخی اوقات این کار عملی نیست. اگر یک انیمیشن واقعاً پیچیده دارید و آن را روی یک رایانه قدیمی و کُند اجرا میکنید، نرخ فریم ممکن است کمتر باشد. ()requestAnimationFrame همواره بیشترین تلاش خود را میکند.
تفاوت ()requestAnimationFrame با ()setInterval و ()setTimeout چیست؟
در این بخش در مورد تفاوت متد ()setInterval و ()setTimeout با دیگر متدهایی که قبلاً بررسی کردیم صحبت میکنیم. اگر به کد قبلی خود نگاهی بیندازیم:
1function ()draw {
2 // Drawing code goes here
3 requestAnimationFrame(draw);
4}
5
6()draw;
میبینیم که چگونه میتوان از مفهوم مشابهی به کمک ()setInterval استفاده کرد:
1function ()draw {
2 // Drawing code goes here
3}
4
5setInterval(draw, 17);
چنان که پیشتر گفتیم، ما هیچ بازه زمانی برای ()requestAnimationFrame تعیین نمیکنیم، این متد با حداکثر سرعت ممکن در شرایط فعلی اجرا میشود. مرورگر نیز در زمانهایی که انیمیشن از حوزه دید خارج شده است، زمان خود را برای اجرای آن به هدر نمیدهد.
در سمت دیگر ()setInterval نیازمند این است که بازهای تعیین شده باشد. ما با فرمول 1000/16 به عدد فریم 17 رسیدیم و آن را به سمت بالا گرد کردیم. رند کردن به سمت بالا به این دلیل صورت گرفته است که اگر آن را به سمت پایینتر رند کنیم ممکن است مرورگر انیمیشن را با نرخی بالاتر از 60 فریم بر ثانیه اجرا کند و میدانیم که این کار هیچ تفاوتی در روانی و سرعت اجرای انیمیشن نخواهد داشت. چنان که پیشتر گفتیم 60 هرتز، نرخ رفرش استاندارد است.
گنجاندن یک ثابت زمانی
Callback واقعی که به تابع ()requestAnimationFrame ارسال میشود میتواند یک پارامتر دیگر نیز داشته باشد و آن مقدار ثابت زمانی است که نشاندهنده زمان گذشته از شروع به کار ()requestAnimationFrame است. این وضعیت مفید است، زیرا امکان اجرای موارد مختلف در زمانهای معین و سرعت ثابت را صرفنظر از این که دستگاه شما چه قدر سریع یا کند است میدهد. الگوی عمومی که میتوان استفاده کرد چیزی مانند زیر است:
1let startTime = null;
2
3function draw(timestamp) {
4 if(!startTime) {
5 startTime = timestamp;
6 }
7
8 currentTime = timestamp - startTime;
9
10 // Do something based on current time
11
12 requestAnimationFrame(draw);
13}
14
15()draw;
پشتیبانی از مرورگر
()requestAnimationFrame در مرورگرهای نسبتاً جدیدتر به جای ()setInterval()/setTimeout پشتیبانی میشود. نکته جالبتر این است که در اینترنت اکسپلورر نسخه 10 به بالا نیز وجود دارد. بنابراین به جز مواردی که میخواهید از مرورگرهای قدیمیتر مانند نسخههای قدیمی IE پشتیبانی کنید، هیچ دلیل برای عدم استفاده از ()requestAnimationFrame دیده نمیشود.
یک مثال ساده
هر چه تا به اینجا از تئوری صحبت کردیم کافی است، در ادامه میخواهیم مثال ()requestAnimationFrame خودمان را بسازیم. به این منظور قصد داریم یک انیمیشن اسپینر ساده بسازیم. این نوعی از انیمیشن است که در یک اپلیکیشن در زمانی که مشغول کاری مانند اتصال به سرور یا کارهای زمانگیری از این دست است استفاده میشود.
نکته: در مثالهای واقعی احتمالاً باید از انیمیشنهای CSS برای اجرای این نوع از انیمیشنها بهره بگیرید. با این حال، این نوع از مثال برای نمایش کاربرد ()requestAnimationFrame کاملاً مفید است. احتمال استفاده از این نوع تکنیک جهت اجرای چیزی پیچیدهتر مانند بهروزرسانی صفحه نمایش یک بازی در هر فریم بیشتر است.
قبل از هر چیز یک قالب خالی HTML مانند کد زیر ایجاد کنید:
1html {
2 background-color: white;
3 height: 100%;
4}
5
6body {
7 height: inherit;
8 background-color: red;
9 margin: 0;
10 display: flex;
11 justify-content: center;
12 align-items: center;
13}
14
15div {
16 display: inline-block;
17 font-size: 10rem;
18}
یک عنصر خالی <div> درون <body> ایجاد کنید و سپس یک کاراکتر ↻ درون آن اضافه کنید. این یک فلش کاراکتر کروی است که از آن به عنوان اسپینر در مثال ساده خود استفاده میکنیم.
CSS زیر را روی قالب HTML به هر طریقی که صلاح میدانید اعمال کنید. بدین ترتیب یک پسزمینه برای صفحه اعمال میشود. ارتفاع body 100 درصد ارتفاع HTML تنظیم میشود و یک <div> درون <body> از نظر افقی و عمودی، به صورت مرکزی تنظیم میشود.
1html {
2 background-color: white;
3 height: 100%;
4}
5
6body {
7 height: inherit;
8 background-color: red;
9 margin: 0;
10 display: flex;
11 justify-content: center;
12 align-items: center;
13}
14
15div {
16 display: inline-block;
17 font-size: 10rem;
18}
یک عنصر <script> را درست بالاتر از تگ <body/> درج کنید.
کد جاوا اسکریپت زیر را درون عنصر <script> درج کنید. در ادامه یک ارجاع به <div> درون یک ثابت ذخیره میکنیم و یک متغیر به نام rotateCount با مقدار 0 تعیین میکنیم و یک متغیر مقداردهی نشده تنظیم میکنیم که در ادامه از سوی آن برای گنجاندن ارجاعی به فراخوانی ()requestAnimationFrame استفاده میکنیم. همچنین مقدار متغیر startTime را null تنظیم میکنیم که در ادامه از آن برای ذخیرهسازی زمان آغاز ()requestAnimationFrame استفاده خواهیم کرد.
1const spinner = document.querySelector('div');
2let rotateCount = 0;
3let rAF;
4let startTime = null;
در زیر کد قبلی، یک تابع ()draw درج کنید. از آن برای گنجاندن کد انیمیشن خود استفاده خواهیم کرد که شامل پارامتر timestamp است:
1function draw(timestamp) {
2
3}
درون تابع ()draw کدهای زیر را وارد کنید. در این کد زمان آغاز از قبل تعریف شده است (این کار در تکرار نخست حلقه اتفاق خواهد افتاد) و کاراکتر اسپینر از طریق افزایش مقدار در هر تکرار به چرخش درمیآید. زمان کنونی از طریق تقسیم زمان آغازین بر 3 به دست میآید و بنابراین سرعت چرخش زیاد سریع نخواهد بود:
1if (!startTime) {
2 startTime = timestamp;
3 }
4
5 let rotateCount = (timestamp - startTime) / 3;
6 spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
در زیر کد قبلی و درون تابع ()draw بلوک کد زیر را نیز اضافه کنید. بدین ترتیب میبینید که آیا مقدار rotateCount بالاتر از 359 است یا نه (چون 360 درجه به معنی یک دور چرخش کامل است) اگر چنین باشد، 360 را از مقدار مورد نظر کسر میکنیم و بنابراین انیمیشن دایره میتواند بدون وقفه و با مقدار معقول اندکی ادامه یابد. توجه داشته باشید که این کار ضرورتی ندارد، اما کار با مقادیر بین 0 تا 359 درجه بسیار آسانتر از مقادیری مانند 1280000 درجه است.
1if (rotateCount > 359) {
2 rotateCount -= 360;
3}
در انتهای بلوک تابع ()draw، خط زیر را قرار دهید. این کلید کل عملیات است، چون میخواهیم متغیری که قبلاً در یک فراخوانی فعال ()requestAnimation تعریف کردیم و تابع ()draw را به عنوان پارامتر خود میگیرد تعیین کنیم. بدین ترتیب انیمیشن آغاز به کار کرده و به صورت مرتب تابع ()draw را تا حد امکان با نرخی نزدیک به 60 FPS اجرا میکند.
1rAF = requestAnimationFrame(draw);
سورس کد کامل این مثال را در ادامه ملاحظه میکنید:
1<!DOCTYPE html>
2<html lang="en-US">
3 <head>
4 <meta charset="utf-8">
5 <title>requestAnimationFrame spinner example</title>
6 <style>
7 html {
8 background-color: white;
9 height: 100%;
10 }
11 body {
12 height: inherit;
13 background-color: red;
14 margin: 0;
15 display: flex;
16 justify-content: center;
17 align-items: center;
18 }
19 div {
20 display: inline-block;
21 font-size: 10rem;
22 }
23 </style>
24 </head>
25 <body>
26
27 <div>↻</div>
28 <script>
29 // Store reference to the div element, create a rotate counter and null startTime
30 // and create an uninitialized variable to store the requestAnimationFrame() call in
31 const spinner = document.querySelector('div');
32 let rotateCount = 0;
33 let startTime = null;
34 let rAF;
35 // Create a draw() function
36 function draw(timestamp) {
37 if(!startTime) {
38 startTime = timestamp;
39 }
40 let rotateCount = (timestamp - startTime) / 3;
41 // Set the rotation of the div to be equal to rotateCount degrees
42 spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
43 // If rotateCount gets to 360, set it back to 0, set it back to 0
44 if(rotateCount > 359) {
45 rotateCount -= 360;
46 }
47 // Call the next frame in the animation
48 rAF = requestAnimationFrame(draw);
49 }
50 draw();
51 </script>
52 </body>
53</html>
پاکسازی یک فراخوانی ()requestAnimationFrame
پاک کردن یک فراخوانی ()requestAnimationFrame از سوی فراخوانی متد ()cancelAnimationFrame صورت میگیرد. دقت کنید که کلمه ابتدایی cancel است و نه clear که در متدهای دیگر ...set رایج بود. مقدار شناسه بازگشتی از فراخوانی ()requestAnimationFrame که در متغیری به نام rAF ذخیره شده است به این متد ارسال میشود:
1cancelAnimationFrame(rAF);
یادگیری عملی: شروع و توقف اسپینر
در این تمرین، ما میخواهیم متد ()cancelAnimationFrame خود را با در نظر گرفتن مثال قبلی و بهروزرسانی آن و افزودن یک شنونده تست کنیم. در زمان کلیک شدن ماوس در هر جای صفحه چرخش اسپینر شروع و یا متوقف میشود.
سرنخهای زیر برای اجرایی کردن این کار ارائه شدهاند:
- یک دستگیره رویداد click به عناصر ماوس اضافه میشود که شامل <body> سند است. اگر میخواهید ناحیه قابل کلیک را به بیشترین حد ممکن برسانید، قرار دادن آن در عنصر <body> مناسب خواهد بود. به این ترتیب این رویداد در فرزندان آن عنصر نیز اجرا میشود.
- میتوانید متغیر ردگیری را نیز اضافه کنید تا ببینید آیا اسپینر میچرخد یا نه، اگر چنین بود فریم را پاک کنید و در غیر این صورت آن را مجدداً فراخوانی کنید.
نکته: تلاش کنید تمرین فوق را خودتان اجرا کنید، اما اگر فکر میکنید موفق نخواهید بود میتوانید کد کامل را در سورس زیر ملاحظه کنید:
1<!DOCTYPE html>
2<html lang="en-US">
3 <head>
4 <meta charset="utf-8">
5 <title>Start and stop spinner example</title>
6 <style>
7 html {
8 background-color: white;
9 height: 100%;
10 }
11 body {
12 height: inherit;
13 background-color: red;
14 margin: 0;
15 display: flex;
16 justify-content: center;
17 align-items: center;
18 }
19 div {
20 display: inline-block;
21 font-size: 10rem;
22 }
23 </style>
24 </head>
25 <body>
26
27 <div>↻</div>
28 <script>
29 // Store reference to the div element, create a rotate counter and null startTime
30 // and create an uninitialized variable to store the requestAnimationFrame() call in
31 const spinner = document.querySelector('div');
32 let rotateCount = 0;
33 let rAF;
34 let startTime = null;
35 // Boolean variable to store state of spinner — true is spinning, false is not spinning
36 let spinning = false;
37 // Create a draw() function
38 function draw(timestamp) {
39 if(!startTime) {
40 startTime = timestamp;
41 }
42 let rotateCount = (timestamp - startTime) / 3;
43 // Set the rotation of the div to be equal to rotateCount degrees
44 spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
45 // If rotateCount gets to 360, set it back to 0, set it back to 0
46 if(rotateCount > 359) {
47 rotateCount -= 360;
48 }
49 // Call the next frame in the animation
50 rAF = requestAnimationFrame(draw);
51 }
52 // event listener to start and stop spinner when page is clicked
53 document.body.addEventListener('click', () => {
54 if(spinning) {
55 cancelAnimationFrame(rAF);
56 spinning = false;
57 } else {
58 draw();
59 spinning = true;
60 }
61 })
62 </script>
63 </body>
64</html>
محدودسازی انیمیشن ()requestAnimationFrame
یکی از مشکلات ()requestAnimationFrame این است که نمیتوانید نرخ فریم خود را تعیین کنید. البته این وضعیت در اغلب موارد مشکلی محسوب نمیشود، چون به طور کلی میخواهیم انیمیشن تا حد امکان روان اجرا شود، اما اگر بخواهید یک انیمیشن 8 بیتی به سبک قدیم بسازید، متوجه خواهید شد که این مسئله مشکلزا است.
برای نمونه این وضعیت در انیمیشن زیر یک مشکل محسوب میشود:
در این مثال باید هم موقعیت کاراکتر را روی صفحه انیمیت کنیم و هم تصویر sprite دیده شود. تنها 6 فریم در انیمیشن sprite وجود دارد. اگر بخواهیم یک فریم sprite متفاوت را در هر فریم که روی صفحه نمایش مییابد با استفاده از ()requestAnimationFrame نمایش دهیم، کاراکتر مربوطه اندامهای خود را چنان به سرعت جابجا میکند که انیمیشن ظاهر عجیبی پیدا میکند. از این رو چرخههای تصویر را با استفاده از کد زیر محدود میکنیم:
1if (posX % 13 === 0) {
2 if (sprite === 5) {
3 sprite = 0;
4 } else {
5 sprite++;
6 }
7}
بنابراین ما صرفاً یک sprite را هر 13 فریم یک بار انیمیت میکنیم. بدین ترتیب در واقع نرخ فریم به 6.5 fps کاهش پیدا میکند چون posX (موقعیت کاراکتر روی صفحه) را در هر فریم بهروزرسانی میکنیم:
1if(posX > width/2) {
2 newStartPos = -((width/2) + 102);
3 posX = Math.ceil(newStartPos / 13) * 13;
4 console.log(posX);
5} else {
6 posX += 2;
7}
این کدی است که شیوه بهروزرسانی موقعیت را در هر فریم انیمیشن نمایش میدهد. متدی که شما برای محدودسازی انیمیشن استفاده میکنید به کد خاص آن مربوط خواهد بود. برای نمونه در مثال اسپینر میتوانستیم آن را صرفاً با افزایش rotateCount به میزان یک فریم به جای دو فریم، طوری طراحی کنیم که کندتر به نظر برسد.
یادگیری عملی: بازی واکنش
در بخش نهایی این مقاله یک بازی واکنشی 2 نفره ایجاد میکنیم. در این بازی دو بازیکن داریم که یکی از آنها بازی را با استفاده از کلید A کنترل میکند و دیگری بازی را با کلید L کنترل خواهد کرد.
زمانی که دکمه Start فشرده شود، یک اسپینر مانند آن که قبلاً دیدیم، به صورت تصادفی برای مدت زمان تصادفی بین 5 تا 10 ثانیه نمایش مییابد. پس از طی شدن این زمان پیامی ظاهر میشود که عبارت «!!PLAYERS GO» را نمایش میدهد. زمانی که این اتفاق بیفتد، بازیکن نخست باید دکمه کنترل خود را در بازی فشار دهد تا برنده شود.
قبل از هر چیز فایل اولیه این اپلیکیشن بازی را روی سیستم خود کپی کنید:
1<!DOCTYPE html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>2-player reaction game</title>
6 <style>
7 /* General styles */
8 html {
9 background-color: white;
10 height: 100%;
11 font-family: sans-serif;
12 }
13 body {
14 height: inherit;
15 background-color: red;
16 margin: 0;
17 }
18 * {
19 box-sizing: border-box;
20 }
21 /* UI Layout */
22 section {
23 width: 100%;
24 height: inherit;
25 padding: 30px;
26 }
27 .topbar {
28 height: 50%;
29 display: flex;
30 justify-content: space-between;
31 }
32 .topbar p, button {
33 margin: 0;
34 font-size: 1.5rem;
35 border: 5px solid;
36 border-radius: 20px;
37 padding: 10px 20px;
38 }
39 .p1, .p2 {
40 align-self: flex-start;
41 }
42 .topbar .p1 {
43 order: 0;
44 border-color: yellow;
45 color: yellow;
46 }
47 .topbar .p2 {
48 order: 2;
49 border-color: cyan;
50 color: cyan;
51 }
52 .topbar .middlebar {
53 order: 1;
54 }
55 .middlebar {
56 display: flex;
57 flex-direction: column;
58 align-items: center;
59 justify-content: space-between;
60 }
61 /* Button-specific styling */
62 button {
63 border: 0;
64 padding: 12.75px 20px;
65 background-color: #ddd;
66 cursor: pointer;
67 }
68 button:hover, button:focus {
69 background-color: #eee;
70 }
71 button:active {
72 background-color: #fff;
73 }
74 /* spinner-specific styling */
75 .spinner {
76 position: absolute;
77 z-index: 1;
78 top: 0;
79 left: 0;
80 right: 0;
81 bottom: 0;
82 }
83 .spinner div {
84 height: 100%;
85 display: flex;
86 align-items: center;
87 justify-content: center;
88 }
89 .spinner p {
90 margin: 0;
91 font-size: 10rem;
92 }
93 </style>
94 </head>
95 <body>
96
97 <div class="spinner"><div><p>↻</p></div></div>
98
99 <section class="ui">
100 <div class="topbar">
101 <p class="p1">Player 1: "A"</p>
102 <p class="p2">Player 2: "L"</p>
103 <div class="middlebar">
104 <button>Start game</button>
105 <p class="result"></p>
106 </div>
107 </div>
108 </section>
109 <script>
110 </script>
111 </body>
112</html>
این فایل شامل ساختار کامل HTML و استایلدهی CSS است که یک صفحه بازی در اختیار ما قرار میدهد و اطلاعات دو بازیکن روی آن نمایش یافتهاند. اما زمانی که اسپینر به چرخش درمیآید، پاراگراف نتایج روی پاراگراف دیگر نمایش پیدا میکند. اینک باید کد جاوا اسکریپت را بنویسیم.
درون عنصر <script> روی صفحه، کار خود را با اضافه کردن خطوط کد زیر برای تعریف برخی ثابتها و متغیرها که در ادامه کد نیاز خواهیم داشت آغاز میکنیم:
1const spinner = document.querySelector('.spinner p');
2const spinnerContainer = document.querySelector('.spinner');
3let rotateCount = 0;
4let startTime = null;
5let rAF;
6const btn = document.querySelector('button');
7const result = document.querySelector('.result');
در ادامه به ترتیب کارهای زیر را انجام دهید:
- یک ارجاع به اسپینر بسازید به طوری که بتوان آن را انیمیت کرد.
- یک ارجاع به عنصر <div> بسازید که شامل اسپینر باشد و برای نمایش یا مخفی کردن آن استفاده میشود.
- یک شمارنده چرخش بسازید که میزان چرخشی که میخواهیم اسپینر در هر فریم از انیمیشن داشته باشد نمایش میدهد.
- یک زمان آغاز null ایجاد کنید که در زمان شروع چرخش اسپینر، شروع به کار خواهد کرد.
- یک متغیر مقداردهی نشده در ادامه فراخوانی ()requestAnimationFrame را که اقدام به انیمیت اسپینر میکند ذخیره میسازد.
- یک ارجاع به دکمه شروع بسازید.
- یک ارجاع به پاراگراف نتایج بسازید.
سپس زیر خطوط قبلی کد، تابع زیر را اضافه کنید. این تابع صرفاً ورودیهای عددی را میگیرد و یک عدد تصادفی بین آن دو بازگشت میدهد. ما باید یک بازه timeout تصادفی در ادامه تولید کنیم:
1function random(min,max) {
2 var num = Math.floor(Math.random()*(max-min)) + min;
3 return num;
4}
سپس تابع ()draw را اضافه میکنیم که اسپینر را انیمیت میکند. این دقیقاً همان نسخهای است که در مثال اسپینر قبلی دیدیم:
1function draw(timestamp) {
2 if(!startTime) {
3 startTime = timestamp;
4 }
5
6 let rotateCount = (timestamp - startTime) / 3;
7 spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
8
9 if(rotateCount > 359) {
10 rotateCount -= 360;
11 }
12
13 rAF = requestAnimationFrame(draw);
14 }
اکنون زمان آن رسیده است که حالت آغازین اپلیکیشن را در زمان بارگذاری اولیه صفحه تنظیم کنیم. خطوط کد زیر را اضافه کنید که به سادگی پاراگراف نتایج و کانتینر اسپینر را با استفاده از ;display: none پنهان میسازد:
1result.style.display = 'none';
2spinnerContainer.style.display = 'none';
ما همچنین یک تابع ()reset تعریف میکنیم که اپلیکیشن را به حالت ابتدایی تنظیم میکند و نیازمند شروع مجدد بازی است. کد زیر را به انتهای کدهای موجود اضافه کنید:
1function reset() {
2 btn.style.display = 'block';
3 result.textContent = '';
4 result.style.display = 'none';
5}
بدین ترتیب مراحل آمادهسازی به پایان میرسد. در ادامه کاری میکنیم که گیم قابلیت بازی کردن پیدا کند. بلوک کد زیر را اضافه کنید. تابع ()start تابع ()draw را فراخوانی میکند که شروع به چرخاندن اسپینر کرده و آن را در UI نمایش میدهد و دکمه Start را پنهان میکند تا کاربر با کلیک کردن مجدد روی آن چندین نسخه را همزمان اجرا نکند. همچنین تابع فوق یک فراخوانی ()setTimeout را اجرا میکند که به نوبه خود تابع ()setEndgame را پس از مدت زمانی تصادفی بین 5 تا 10 ثانیه اجرا میکند. ضمناً یک شنونده رویداد نیز به دکمه خود اضافه میکنیم تا در زمان کلیک شدن، تابع ()start را اجرا کند.
1btn.addEventListener('click', start);
2
3function start() {
4 draw();
5 spinnerContainer.style.display = 'block';
6 btn.style.display = 'none';
7 setTimeout(setEndgame, random(5000,10000));
8}
نکته: در این مثال دیدیم که ()setTimeout بدون ذخیرهسازی مقدار بازگشتی فراخوانی شد. این وضعیت کار میکند و مشکلی ندارد چون لزومی به پاکسازی interval/timeout در هیچ زمانی وجود ندارد. اگر قصد چنین کاری را داشته باشید، باید شناسه بازگشتی را ذخیره کنید.
نتیجه نهایی کد قبلی این است که وقتی دکمه Start فشرده شود، اسپینر، نمایش مییابد و بازیکنان باید مدت زمانی تصادفی منتظر بمانند تا از آنها خواسته شود دکمهشان را فشار دهند. این بخش آخر به وسیله تابع ()setEndgame مدیریت میشود که در ادامه تعریف میکنیم:
در این مرحله تابع زیر را به ادامه کد خود اضافه کنید:
1function setEndgame() {
2 cancelAnimationFrame(rAF);
3 spinnerContainer.style.display = 'none';
4 result.style.display = 'block';
5 result.textContent = 'PLAYERS GO!!';
6
7 document.addEventListener('keydown', keyHandler);
8
9 function keyHandler(e) {
10 console.log(e.key);
11 if(e.key === 'a') {
12 result.textContent = 'Player 1 won!!';
13 } else if(e.key === 'l') {
14 result.textContent = 'Player 2 won!!';
15 }
16
17 document.removeEventListener('keydown', keyHandler);
18 setTimeout(reset, 5000);
19 };
20}
مراحل کار به صورت زیر است:
- ابتدا انیمیشن اسپینر را با ()cancelAnimationFrame لغو میکنیم (پاک کردن پردازشهای غیر لازم همواره رویهای خوب محسوب میشود) و کانتینر اسپینر را نیز پنهان میکنیم.
- سپس پاراگراف نتایج را نمایش میدهیم و متن آن را به صورت «!!PLAYERS GO» تنظیم میکنیم تا به بازیکنها اعلام کنیم که اینک میتوانند دکمه را برای برنده شدن فشار دهند.
- سپس یک شنونده رویداد keydown به سند خود الصاق میکنیم تا زمانی که دکمه فشرده شود، تابع ()keyHandler اجرا شود.
- درون تابع ()keyHandler یک شیء رویداد به صورت یک پارامتر (با e نمایش مییابد) قرار میدهیم. خصوصیت key آن شامل کلیدی است که یکی از بازیکنها فشرده است و میتوانیم از آن به وسیله اقدامهای خاصی برای پاسخ دادن به کلید خاصی که پردازش شده است استفاده کنیم.
- ابتدا e.key را در کنسول لاگ میکنیم که روش مفیدی برای یافتن مقدار کلید مختلفی که زده شده محسوب میشود.
- زمانی که e.key شامل کاراکتر a باشد، یک پیام نمایش میدهیم که اعلام میکند بازیکن اول برنده شده است و زمانی که e.key شامل کلید l باشد اعلام میکنیم که بازیکن دوم برنده شده است. توجه کنید که این وضعیت تنها زمانی کار میکند که از حروف کوچک a و l استفاده شده باشد. اگر حروف بزرگ A و L فشرده شده باشند به عنوان کلید متفاوتی به حساب میآیند.
- صرفنظر از این که کدام یک از بازیکنها کلید کنترل را فشار دادهاند، شنونده رویداد keydown را با استفاده از ()removeEventListener حذف میکنیم تا زمانی که در ادامه این کلیدها فشرده شوند اختلالی در روند بازی ایجاد نکنند. همچنین از ()setTimeout برای فراخوانی ()reset پس از 5 ثانیه استفاده میکنیم. چنان که قبلاً توضیح دادیم، این تابع بازی را به حالت اولیهاش ریست میکند و بازیکنها میتوانند بازی جدیدی را از ابتدا شروع کنند.
بدین ترتیب کار طراحی این تمرین به پایان میرسد. اگر هر گونه اشکالی در این تمرین داشتید میتوانید از سورس کد کامل زیر کمک بگیرید:
1<!DOCTYPE html>
2<html lang="en-US">
3 <head>
4 <meta charset="utf-8">
5 <title>2-player reaction game</title>
6 <style>
7 /* General styles */
8 html {
9 background-color: white;
10 height: 100%;
11 font-family: sans-serif;
12 }
13 body {
14 height: inherit;
15 background-color: red;
16 margin: 0;
17 }
18 * {
19 box-sizing: border-box;
20 }
21 /* UI Layout */
22 section {
23 width: 100%;
24 height: inherit;
25 padding: 30px;
26 }
27 .topbar {
28 height: 50%;
29 display: flex;
30 justify-content: space-between;
31 }
32 .topbar p, button {
33 margin: 0;
34 font-size: 1.5rem;
35 border: 5px solid;
36 border-radius: 20px;
37 padding: 10px 20px;
38 }
39 .p1, .p2 {
40 align-self: flex-start;
41 }
42 .topbar .p1 {
43 order: 0;
44 border-color: yellow;
45 color: yellow;
46 }
47 .topbar .p2 {
48 order: 2;
49 border-color: cyan;
50 color: cyan;
51 }
52 .topbar .middlebar {
53 order: 1;
54 }
55 .middlebar {
56 display: flex;
57 flex-direction: column;
58 align-items: center;
59 justify-content: space-between;
60 }
61 /* Button-specific styling */
62 button {
63 border: 0;
64 padding: 12.75px 20px;
65 background-color: #ddd;
66 cursor: pointer;
67 }
68 button:hover, button:focus {
69 background-color: #eee;
70 }
71 button:active {
72 background-color: #fff;
73 }
74 /* spinner-specific styling */
75 .spinner {
76 position: absolute;
77 z-index: 1;
78 top: 0;
79 left: 0;
80 right: 0;
81 bottom: 0;
82 }
83 .spinner div {
84 height: 100%;
85 display: flex;
86 align-items: center;
87 justify-content: center;
88 }
89 .spinner p {
90 margin: 0;
91 font-size: 10rem;
92 }
93 </style>
94 </head>
95 <body>
96
97 <div class="spinner"><div><p>↻</p></div></div>
98
99 <section class="ui">
100 <div class="topbar">
101 <p class="p1">Player 1: "A"</p>
102 <p class="p2">Player 2: "L"</p>
103 <div class="middlebar">
104 <button>Start game</button>
105 <p class="result"></p>
106 </div>
107 </div>
108 </section>
109 <script>
110 // Store reference to the spinner and spinner container, create a rotate counter and null startTime
111 // and create an uninitialized variable to store a requestAnimationFrame() call in,
112 const spinner = document.querySelector('.spinner p');
113 const spinnerContainer = document.querySelector('.spinner');
114 let rotateCount = 0;
115 let startTime = null;
116 let rAF;
117 // Store references to the start button and the result paragraph
118 const btn = document.querySelector('button');
119 const result = document.querySelector('.result');
120 // function to generate random number
121 function random(min,max) {
122 var num = Math.floor(Math.random()*(max-min)) + min;
123 return num;
124 }
125 // Create a draw() function
126 function draw(timestamp) {
127 if(!startTime) {
128 startTime = timestamp;
129 }
130 let rotateCount = (timestamp - startTime) / 3;
131 // Set the rotation of the div to be equal to rotateCount degrees
132 spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
133 // If rotateCount gets to 360, set it back to 0, set it back to 0
134 if(rotateCount > 359) {
135 rotateCount -= 360;
136 }
137 // Call the next frame in the animation
138 rAF = requestAnimationFrame(draw);
139 }
140 // Initially hide the spinner and results
141 result.style.display = 'none';
142 spinnerContainer.style.display = 'none';
143 // Reset the game to its initial state on restart
144 function reset() {
145 btn.style.display = 'block';
146 result.textContent = '';
147 result.style.display = 'none';
148 }
149 // Start the game when the button is pressed
150 btn.addEventListener('click', start);
151 function start() {
152 // Start the spinner spinning
153 draw();
154 // Show the spinner and hide the button
155 spinnerContainer.style.display = 'block';
156 btn.style.display = 'none';
157 // run the setEndgame() function after a random number of seconds between 5 and 10
158 setTimeout(setEndgame, random(5000,10000));
159 }
160 // Function to allow players to take their turn when the time is right
161 function setEndgame() {
162 cancelAnimationFrame(rAF);
163 spinnerContainer.style.display = 'none';
164 result.style.display = 'block';
165 result.textContent = 'PLAYERS GO!!';
166 document.addEventListener('keydown', keyHandler);
167 function keyHandler(e) {
168 console.log(e.key);
169 if(e.key === "a") {
170 result.textContent = 'Player 1 won!!';
171 } else if(e.key === "l") {
172 result.textContent = 'Player 2 won!!';
173 }
174 document.removeEventListener('keydown', keyHandler);
175 setTimeout(reset, 5000);
176 };
177 }
178 </script>
179 </body>
180</html>
سخن پایانی
به این ترتیب به پایان این بخش از سری مقالات آموزش جاوا اسکریپت میرسیم. همه موارد ضروری حلقههای ناهمگام و بازهها در این مقاله توضیح داده شدهاند. این متدها در موقعیتهای مختلف کدنویسی به کار میآیند، اما مراقب باشید که از آنها بیش از حد استفاده نکنید. چون این کدها همچنان در نخ اصلی اجرا میشوند و callback-های سنگین و شدید به خصوص آنهایی که مستلزم دستکاری DOM هستند معمولاً اگر مراقب نباشید موجب کند شدن صفحه میشوند.
برای مطالعه قسمت بعدی این مجموعه مطلب آموزشی میتوانید روی لینک زیر کلیک کنید:
اگر این مطلب برای شما مفید بوده است، آمورش های زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای جاوا اسکریپت
- مجموعه آموزشهای برنامهنویسی
- آموزش JavaScript ES6 (جاوا اسکریپت)
- آموزش جاوا اسکریپت — مجموعه مقالات جامع وبلاگ فرادرس
- رایج ترین روش های ایجاد درخواست HTTP در جاوا اسکریپت — راهنمای مقدماتی
==