برنامه نویسی 2713 بازدید

در این مقاله به بررسی متدهای کلاسیک جاوا اسکریپت از قبیل Timeout و Interval که برای اجرای کد ناهمگام پس از گذشت زمان معین یا در بازه‌های مشخصی از زمان کاربرد دارند اشاره می‌کنیم؛ همچنین در مورد فایده آن‌ها صحبت می‌کنیم و مشکلات ذاتی‌شان را مورد بررسی قرار می‌دهیم. برای مطالعه بخش قبلی این سری مقالات آموزشی به لینک زیر بروید:

پیش‌نیازها

  • سواد مقدماتی رایانه
  • درک معقولی از مبانی جاوا اسکریپت

هدف از مطالعه این مقاله درک حلقه‌ها و بازه‌های ناهمگام و موارد کاربرد آن‌ها است.

مقدمه

برای مدتی طولانی، پلتفرم وب چند تابع را در اختیار برنامه نویسان وب قرار داده بود که امکان اجرای ناهمگام کد را پس از گذشت مدت زمان معینی فراهم می‌ساختند. همچنین با این تابع‌ها می‌توانستید به اجرای مکرر بلوک کد به صورت ناهمگام تا زمانی که دستور توقف داده نشده بود بپردازید. آن‌ها شامل موارد زیر هستند:

  • ()setTimeout: این تابع یک بلوک کد را پس از گذشت مدت زمان معینی اجرا می‌کند.
  • ()setInterval: این تابع یک بلوک کد را به طور مکرر و با تأخیر زمانی ثابتی بین هر فراخوانی اجرا می‌کند.
  • ()requestAnimationFrame: نسخه مدرن ()setInterval است و بلوک کد معینی را پیش از آن که مرورگر بار دیگر ظاهر خود را بازترسیم کند اجرا می‌کند. بدین ترتیب یک انیمیشن را می‌توان با نرخ فریم مناسب صرف‌نظر از محیطی که در آن اجرا می‌شود به دست آورد.

کد ناهمگام در عمل از سوی این تابع‌ها روی نخ اصلی اجرا می‌شود، اما می‌توان کد را بین تکرارها نیز اجرا کرد تا درجه کارایی کمتر یا بیشتری داشته باشد و همه این‌ها به میزان سنگین بودن این کارها از نظر محاسبات پردازنده وابسته هستند. در هر حال، این تابع‌ها برای اجرای مداوم انیمیشن‌ها و دیگر پردازش‌های پس‌زمینه روی یک وب‌سایت یا وب اپلیکیشن مورد استفاده قرار می‌گیرند. در بخش‌های بعدی روش استفاده از آن‌ها را نمایش خواهیم داد.

()setTimeout

چنان که قبلاً گفتیم، ()setTimeout یک بلوک کد را پس از گذشت مدت زمان معینی اجرا می‌کند. پارامترهای این تابع به صورت زیر هستند:

  • یک تابع که باید اجرا شود و یا یک ارجاع به تابعی که در جای دیگری تعریف شده است.
  • یک عدد که نشان‌دهنده بازه زمانی بر حسب میلی‌ثانیه (بدین ترتیب 1000 به معنی یک ثانیه است) است که باید پیش از اجرای کد منتظر ماند. اگر مقدار 0 ارسال شود (یا کلاً این مقدار نادیده گرفته شود) تابع بی‌درنگ اجرا خواهد شد. دلیل این که چرا باید چنین چیزی را خواست، در ادامه بیشتر توضیح می‌دهیم.
  • در ادامه می‌توان هیچ پارامتری نداشت یا به چند پارامتر دیگر اشاره کرد که قصد داریم در زمان اجرا به تابع ارسال کنیم.

نکته: از آنجا که timeout callback-‌ها با همکاری متقابل با یکدیگر اجرا می‌شوند، هیچ تضمینی وجود ندارد که پس از دقیقاً مقدار معینی از زمان اجرا شوند. اما می‌توان مطمئن بود که پس از دست کم زمان تعیین شده اجرا خواهند شد. دستگیره‌های timeout نمی‌توانند تا زمانی که نخ به نقطه‌ای در اجرای خود نرسیده است که از این دستگیره‌ها برای یافتن موارد مورد نیاز برای اجرا استفاده کند، قابل اجرا نخواهند بود.

در مثال زیر، مرورگر پیش از اجرای تابع بی‌نام دو ثانیه منتظر می‌ماند، و سپس پیام هشدار را نمایش می‌دهد:

تابع‌هایی که استفاده می‌کنیم لزوماً نباید بی‌نام باشند. می‌توان یک تابع دارای نام نیز ارائه کرد و حتی می‌توان این تابع را در جای دیگری تعریف کرد و ارجاع تابع را به ()setTimeout ارسال کرد. در ادامه دو نسخه از قطعه کد معادل کد اول را می‌بینید:

این وضعیت به طور مثال در صورتی که تابعی داشته باشیم که لازم است از یک timeout و همچنین از یک پاسخ به رویداد قابل فراخوانی باشد، مفید خواهد بود. اما این وضعیت به خصوص اگر timeout callback از چند خط کد تشکیل یافته باشد، می‌تواند به انسجام کد نیز کمک کند.

()setTimeout یک مقدار شناسه بازگشت می‌دهد که می‌توان از آن در ادامه، مثلاً زمانی که می‌خواهیم آن را متوقف کنیم، برای اشاره به Timeout استفاده کرد. برای آشنایی با روش انجام این کار به بخش «پاکسازی timeout-ها» در ادامه مراجعه کنید.

ارسال پارامترها به تابع ()setTimeout

هر پارامتری که بخواهیم به تابعی که درون ()setTimeout اجرا می‌شود، ارسال کنیم باید به صورت پارامترهای اضافی در انتهای لیست پارامترهای ()setTimeout، آن‌ها را مورد اشاره قرار دهیم. برای نمونه می‌توانیم تابع قبلی را طوری بازنویسی کنیم که در صورت ارسال نام هر فرد به تابع به او سلام کند:

نام فردی که قرار است به آن سلام کند، درون فراخوانی ()setTimeout به صورت پارامتر سوم ارسال می‌شود:

پاکسازی timeout-ها

در نهایت اگر یک timeout ایجاد شده باشد، می‌توان آن را پیش از گذشت زمان معینی با فراخوانی ()clearTimeout لغو کرد. این کار از طریق ارسال شناسه فراخوانی ()setTimeout به صورت یک پارامتر میسر است. بنابراین برای لغو timeout فوق باید کارهای زیر را انجام دهیم:

نکته: در مثال زیر یک دموی نسبتاً پیچیده‌تر را مشاهده می‌کنید که امکان می‌دهد نام فردی که قرار است به وی سلام شود را در یک فرم تعیین کنیم و با استفاده از دکمه جداگانه‌ای می‌توان خوشامدگویی را لغو کرد.

()setInterval

()setTimeout در مواردی که لازم باشد کد را یک بار و پس از مدت معینی اجرا کنیم به خوبی کار می‌کند. اما وقتی بخواهیم کد را بارها و بارها برای نمونه به صورت یک انیمیشن اجرا کنیم کارایی ندارد.

در این موارد باید از ()setInterval استفاده کنیم. روش کار آن مشابه ()setTimeout است، به جز این که تابعی که به صورت پارامتر اول به آن ارسال می‌شود به صورت مکرر اجرا می‌شود و زمان بین هر اجرا بر اساس مقداری که بر حسب میلی‌ثانیه در پارامتر دوم اشاره شده تعیین می‌شود. همچنین می‌توان هر پارامتری که قرار است از سوی تابع، متعاقباً استفاده شود را در پارامترهای بعدی فراخوانی ()setInterval ارسال کرد.

به مثال زیر توجه کنید. تابع زیر یک شیء ()Date ایجاد می‌کند، رشته زمانی آن را با استفاده از ()toLocaleTimeString استخراج می‌کند و سپس آن را در UI نمایش می‌دهد. سپس می‌توانیم تابع را هر ثانیه یک بار با استفاده از ()setInterval اجرا کنیم و یک جلوه ساعت دیجیتال بسازیم که هر ثانیه یک بار به‌روز می‌شود. کارکرد کد زیر را می‌توانید در این صفحه (+) مشاهده کنید:

()setInterval نیز دقیقاً همانند ()setTimeout یک مقدار شناسه بازگشت می‌دهد که می‌توانید متعاقباً از آن برای پاکسازی intervals استفاده کنید.

پاکسازی Intervals

()setInterval یک وظیفه را برای همیشه اجرا می‌کند، مگر این که کار دیگری در مورد آن انجام دهیم. ممکن است بخواهیم چنین وظایفی را متوقف کنیم، چون در غیر این صورت ممکن است هنگامی که مرورگرها نتوانند نسخه‌های دیگری از وظیفه را تکمیل کنند با خطا مواجه شویم یا اگر انیمیشنی که از سوی وظیفه مدیریت می‌شود به پایان برسد باید آن را متوقف کنیم. این کار به همان روشی که timeouts را متوقف کردیم، اجرا می‌شود. به این منظور باید شناسه بازگشتی از فراخوانی ()setInterval را به تابع ()clearInterval ارسال کنید:

مثال کاربردی: ایجاد یک کرنومتر

با توجه به همه مطالبی که گفته شد اینک یک چالش برای شما داریم. ابتدا کد زیر را در فایلی به نام setInterval-clock.html روی سیستم خود کپی کنید:

سپس آن را با توجه به مواردی که در ادامه آمده است طوری تغییر دهید که یک کرنومتر برای خودتان بسازید. شما باید زمان را مانند قبل نمایش دهید، اما در این مثال به موارد زیر نیاز داریم:

  • یک دکمه شروع برای آغاز به کار کرنومتر
  • یک دکمه توقف برای ایجاد مکث/توقف در اجرای کرنومتر
  • یک دکمه ریست برای ریست کردن زمان به مقدار صفر.
  • زمان نمایش یافته به جای زمان واقعی، تعداد ثانیه‌های گذشته را ثبت می‌کند.

در ادامه برای اجرای وظایف فوق چند سرنخ در اختیار شما قرار می‌دهیم:

شما می‌توانید نشانه‌گذاری دکمه را بر حسب سلیقه خود استایل‌دهی کنید، فقط باید مطمئن شوید که از HTML معناشناختی استفاده می‌کنید که امکان دریافت ارجاع دکمه را در جاوا اسکریپت فراهم می‌سازد. احتمالاً بخواهید یک متغیر بسازید که از 0 آغاز شود و سپس آن را هر ثانیه یک بار با استفاده از یک حلقه ثابت افزایش دهید.

ایجاد مثال بدون استفاده از شیء ()Date چنان که در نسخه قبلی اجرا کردیم، آسان‌تر است، اما دقت کمتری دارد و نمی‌توان مطمئن بود که callback دقیقاً پس از 1000 میلی‌ثانیه اجرا می‌شود. روش دقیق‌تر می‌تواند اجرای کد ()startTime = Date.now برای دریافت زمان جاری دقیقاً از زمان کلیک کردن دکمه شروع از سوی کاربر باشد و سپس می‌توان از Date.now() – startTime برای دریافت تعداد میلی‌ثانیه‌های سپری شده از زمان کلیک کردن دکمه استفاده کرد.

همچنین می‌توانید تعداد ساعت‌ها، دقایق، ثانیه‌ها را به صورت مقادیر متفاوت محاسبه کنید و سپس آن‌ها را با هم در یک رشته پس از هر تکرار حلقه نمایش دهید. در شمارنده دوم می‌توان هر کدام از این موارد را محاسبه کرد.

شیوه محاسبه آن‌ها چگونه است؟ به موارد زیر فکر کنید:

  • تعداد ثانیه‌های موجود در یک ساعت برابر با 3600 است.
  • تعداد دقایق برابر با مقدار ثانیه‌های باقی‌مانده پس از حذف ساعت‌ها و تقسیم کردن بر 60 خواهد بود.
  • تعداد ثانیه‌ها مقدار ثانیه‌هایی است که پس از حذف همه دقایق باقی می‌مانند.

اگر می‌خواهید ساعت کرنومترتان شبیه کرنومترهای کلاسیک باشد، می‌توانید یک صفر ابتدایی برای نمایش مقادیر کمتر از 10 به ابتدای ارقام مربوطه اضافه کنید.

برای ایجاد مکث در کار کرنومتر باید interval را پاک کنید. برای ریست کردن آن باید شمارنده را مجدداً به 0 تنظیم کنید و بی‌درنگ نمایش را به‌روزرسانی کنید. احتمالاً مجبور باشید دکمه Start را پس از یک بار زدن آن غیرفعال کنید و آن را مجدداً در زمان متوقف شدن کار کرنومتر فعال کنید. در غیر این صورت ممکن است چند نسخه از دکمه استارت به کار افتند و چند نسخه از ()setInterval وجود داشته باشند که منجر به رفتار نادرست می‌شود.

نکته: اگر در نوشتن کد فوق به مشکل برخورد کردید، می‌توانید از سورس کد کامل زیر بهره بگیرید:

نکات مهم در مورد ()setTimeout و ()setInterval

چند نکته وجود دارند که هنگام کار کردن با ()setTimeout و ()setInterval باید به خاطر بسپارید. این موارد را در ادامه معرفی می‌کنیم.

Timeout-های بازگشتی

روش دیگری نیز برای استفاده از ()setTimeout وجود دارد. می‌توان آن را به صورت بازگشتی اجرا کرد تا کد یکسانی را به صورت مکرر اجرا کند. در این روش دیگر نیازی به استفاده از ()setInterval نیست.

در مثال زیر از یک ()setTimeout بازگشتی برای اجرای تابع ارسالی در هر 100 میلی‌ثانیه یک بار استفاده شده است:

مثال فوق را با مثال زیر مقایسه کنید که در آن از ()setInterval برای اجرای تأثیر مشابه استفاده شده است:

تفاوت ()setTimeout بازگشتی و ()setInterval چیست؟

تفاوت بین دو نسخه از کد فوق جزئی است:

  • ()setTimeout بازگشتی تضمین می‌کند تأخیر یکسانی بین اجراهای مختلف وجود دارد. برای نمونه در کد فوق این تأخیر همواره 100 میلی‌ثانیه خواهد بود. کد اجرا می‌شود و سپس 100 میلی‌ثانیه صبر می‌کند تا مجدداً اجرا شود، بنابراین بازه زمانی صرف‌نظر از این که کد چه مقدار اجرا شود یکسان خواهد بود.
  • مثال فوق با استفاده از ()setInterval کارها را به ترتیب متفاوتی اجرا می‌کند. بازه‌ای که ما انتخاب کرده‌ایم شامل زمانی است که برای اجرای کد صرف می‌شود. یعنی اگر این کد برای اجرا به 40 ثانیه زمان نیاز داشته باشد، بازه مزبور در طی 60 ثانیه به پایان می‌رسد.
  • زمانی که به صورت بازگشتی از ()setTimeout استفاده می‌کنید، در هر تکرار ممکن است تأخیر متفاوتی پیش از اجرای بعدی محاسبه شود. به بیان دیگر مقدار پارامتر ثانیه را می‌توان زمان متفاوتی بر حسب میلی‌ثانیه تعیین کرد تا پیش از اجرای مجدد کد صبر کند.

هنگامی که احتمال می‌رود اجرای کد زمانی بیش از بازه زمانی تعیین شده طول بکشد، بهتر است از ()setTimeout بازگشتی استفاده شود. بدین ترتیب بازه زمانی بین اجراها صرف‌نظر از زمانی که برای اجرای کد صرف می‌شود، همواره ثابت خواهد بود و از بروز خطا جلوگیری می‌کند.

Timeout-های بی‌درنگ

وقتی از مقدار 0 برای ()setTimeout استفاده می‌کنید، اجرای تابع callback در اولین فرصت ممکن زمان‌بندی می‌شود، اما این کار پس از آن که نخ کد اصلی اجرا شد انجام خواهد یافت.

برای نمونه کد زیر یک پیام با مضمون «hello» در خروجی ارائه می‌کند و سپس زمانی که روی دکمه OK آن کلیک کنید، بی‌درنگ یک پیام با مضمون «World» ارائه می‌کند.

این وضعیت در مواردی که می‌خواهید یک بلوک کد به محض پایان یافتن همه کارهای نخ اصلی اجرا شود، مفید خواهد بود. آن را در یک حلقه رویداد async قرار دهید تا پس از آن به صورت سرراستی اجرا شود.

پاکسازی با ()clearTimeout یا ()clearInterval

()clearTimeout و ()clearInterval هر دو از فهرست یکسانی از مداخل برای پاکسازی فرم استفاده می‌کنند. نکته جالب‌تر این است که می‌توانید از هر کدام از این متدها برای پاکسازی ()clearTimeout یا ()clearInterval استفاده کنید.

برای انسجام کد می‌توان از مدخل‌های ()clearTimeout برای پاکسازی ()setTimeout و از مدخل‌های ()clearInterval برای پاکسازی ()setInterval استفاده کرد. بدین ترتیب از ایجاد سردرگمی اجتناب می‌شود.

()requestAnimationFrame

تابع ()requestAnimationFrame یک تابع حلقه تخصصی برای ایجاد انیمیشن‌های اجرایی به روشی مؤثر در مرورگر محسوب می‌شود. این تابع اساساً نسخه مدرن‌تری از ()setInterval است و یک بلوک کد خاص را پیش از آن که مرورگر، صفحه نمایش را مجدداً ترسیم کند اجرا می‌کند. بدین ترتیب می‌توان یک انیمیشن را با نرخ فریم مناسبی رندر کرد و مهم نیست که محیطی که اجرا می‌شود کدام است.

این تابع در پاسخ به مشکلات رایج ()setInterval طراحی شده است. برای نمونه ()setInterval در نرخ فریم بهینه برای همه دستگاه‌ها اجرا نمی‌شود و در برخی موارد با فقدان فریم مواجه می‌شویم. همچنین این تابع در صورتی که برگه مرورگر فعال نباشد یا انیمیشن به خارج از حوزه دید صفحه اسکرول شود، به اجرای خود ادامه می‌دهد.

متد ()requestAnimationFrame پیش از ترسیم مجدد، یک Callback به عنوان آرگومان می‌گیرد. این الگوی عمومی است که در زمان استفاده مشاهده می‌کنید:

ایده اصلی این است که یک تابع تعریف کنیم که انیمیشن درون آن به‌روزرسانی شود یعنی تصاویر 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 با دیگر متدهایی که قبلاً بررسی کردیم صحبت می‌کنیم. اگر به کد قبلی خود نگاهی بیندازیم:

می‌بینیم که چگونه می‌توان از مفهوم مشابهی به کمک ()setInterval استفاده کرد:

چنان که پیش‌تر گفتیم، ما هیچ بازه زمانی برای ()requestAnimationFrame تعیین نمی‌کنیم، این متد با حداکثر سرعت ممکن در شرایط فعلی اجرا می‌شود. مرورگر نیز در زمان‌هایی که انیمیشن از حوزه دید خارج شده است، زمان خود را برای اجرای آن به هدر نمی‌دهد.

در سمت دیگر ()setInterval نیازمند این است که بازه‌ای تعیین شده باشد. ما با فرمول 1000/16 به عدد فریم 17 رسیدیم و آن را به سمت بالا گرد کردیم. رند کردن به سمت بالا به این دلیل صورت گرفته است که اگر آن را به سمت پایین‌تر رند کنیم ممکن است مرورگر انیمیشن را با نرخی بالاتر از 60 فریم بر ثانیه اجرا کند و می‌دانیم که این کار هیچ تفاوتی در روانی و سرعت اجرای انیمیشن نخواهد داشت. چنان که پیش‌تر گفتیم 60 هرتز، نرخ رفرش استاندارد است.

گنجاندن یک ثابت زمانی

Callback واقعی که به تابع ()requestAnimationFrame ارسال می‌شود می‌تواند یک پارامتر دیگر نیز داشته باشد و آن مقدار ثابت زمانی است که نشان‌دهنده زمان گذشته از شروع به کار ()requestAnimationFrame است. این وضعیت مفید است، زیرا امکان اجرای موارد مختلف در زمان‌های معین و سرعت ثابت را صرف‌نظر از این که دستگاه شما چه قدر سریع یا کند است می‌دهد. الگوی عمومی که می‌توان استفاده کرد چیزی مانند زیر است:

پشتیبانی از مرورگر

()requestAnimationFrame در مرورگرهای نسبتاً جدیدتر به جای ()setInterval()/setTimeout پشتیبانی می‌شود. نکته جالب‌تر این است که در اینترنت اکسپلورر نسخه 10 به بالا نیز وجود دارد. بنابراین به جز مواردی که می‌خواهید از مرورگرهای قدیمی‌تر مانند نسخه‌های قدیمی IE پشتیبانی کنید، هیچ دلیل برای عدم استفاده از ()requestAnimationFrame دیده نمی‌شود.

یک مثال ساده

هر چه تا به اینجا از تئوری صحبت کردیم کافی است، در ادامه می‌خواهیم مثال ()requestAnimationFrame خودمان را بسازیم. به این منظور قصد داریم یک انیمیشن اسپینر ساده بسازیم. این نوعی از انیمیشن است که در یک اپلیکیشن در زمانی که مشغول کاری مانند اتصال به سرور یا کارهای زمان‌گیری از این دست است استفاده می‌شود.

نکته: در مثال‌های واقعی احتمالاً باید از انیمیشن‌های CSS برای اجرای این نوع از انیمیشن‌ها بهره بگیرید. با این حال، این نوع از مثال برای نمایش کاربرد ()requestAnimationFrame کاملاً مفید است. احتمال استفاده از این نوع تکنیک جهت اجرای چیزی پیچیده‌تر مانند به‌روزرسانی صفحه نمایش یک بازی در هر فریم بیشتر است.

قبل از هر چیز یک قالب خالی HTML مانند کد زیر ایجاد کنید:

یک عنصر خالی <div> درون <body> ایجاد کنید و سپس یک کاراکتر ↻ درون آن اضافه کنید. این یک فلش کاراکتر کروی است که از آن به عنوان اسپینر در مثال ساده خود استفاده می‌کنیم.

CSS زیر را روی قالب HTML به هر طریقی که صلاح می‌دانید اعمال کنید. بدین ترتیب یک پس‌زمینه برای صفحه اعمال می‌شود. ارتفاع body 100 درصد ارتفاع HTML تنظیم می‌شود و یک <div> درون <body> از نظر افقی و عمودی، به صورت مرکزی تنظیم می‌شود.

یک عنصر <script> را درست بالاتر از تگ <body/> درج کنید.

کد جاوا اسکریپت زیر را درون عنصر <script> درج کنید. در ادامه یک ارجاع به <div> درون یک ثابت ذخیره می‌کنیم و یک متغیر به نام rotateCount با مقدار 0 تعیین می‌کنیم و یک متغیر مقداردهی نشده تنظیم می‌کنیم که در ادامه از سوی آن برای گنجاندن ارجاعی به فراخوانی ()requestAnimationFrame استفاده می‌کنیم. همچنین مقدار متغیر startTime را null تنظیم می‌کنیم که در ادامه از آن برای ذخیره‌سازی زمان آغاز ()requestAnimationFrame استفاده خواهیم کرد.

در زیر کد قبلی، یک تابع ()draw درج کنید. از آن برای گنجاندن کد انیمیشن خود استفاده خواهیم کرد که شامل پارامتر timestamp است:

درون تابع ()draw کدهای زیر را وارد کنید. در این کد زمان آغاز از قبل تعریف شده است (این کار در تکرار نخست حلقه اتفاق خواهد افتاد) و کاراکتر اسپینر از طریق افزایش مقدار در هر تکرار به چرخش درمی‌آید. زمان کنونی از طریق تقسیم زمان آغازین بر 3 به دست می‌آید و بنابراین سرعت چرخش زیاد سریع نخواهد بود:

در زیر کد قبلی و درون تابع ()draw بلوک کد زیر را نیز اضافه کنید. بدین ترتیب می‌بینید که آیا مقدار rotateCount بالاتر از 359 است یا نه (چون 360 درجه به معنی یک دور چرخش کامل است) اگر چنین باشد، 360 را از مقدار مورد نظر کسر می‌کنیم و بنابراین انیمیشن دایره می‌تواند بدون وقفه و با مقدار معقول اندکی ادامه یابد. توجه داشته باشید که این کار ضرورتی ندارد، اما کار با مقادیر بین 0 تا 359 درجه بسیار آسان‌تر از مقادیری مانند 1280000 درجه است.

در انتهای بلوک تابع ()draw، خط زیر را قرار دهید. این کلید کل عملیات است، چون می‌خواهیم متغیری که قبلاً در یک فراخوانی فعال ()requestAnimation تعریف کردیم و تابع ()draw را به عنوان پارامتر خود می‌گیرد تعیین کنیم. بدین ترتیب انیمیشن آغاز به کار کرده و به صورت مرتب تابع ()draw را تا حد امکان با نرخی نزدیک به 60 FPS اجرا می‌کند.

سورس کد کامل این مثال را در ادامه ملاحظه می‌کنید:

پاکسازی یک فراخوانی ()requestAnimationFrame

پاک کردن یک فراخوانی ()requestAnimationFrame از سوی فراخوانی متد ()cancelAnimationFrame صورت می‌گیرد. دقت کنید که کلمه ابتدایی cancel است و نه clear که در متدهای دیگر …set رایج بود. مقدار شناسه بازگشتی از فراخوانی ()requestAnimationFrame که در متغیری به نام rAF ذخیره شده است به این متد ارسال می‌شود:

یادگیری عملی: شروع و توقف اسپینر

در این تمرین، ما می‌خواهیم متد ()cancelAnimationFrame خود را با در نظر گرفتن مثال قبلی و به‌روزرسانی آن و افزودن یک شنونده تست کنیم. در زمان کلیک شدن ماوس در هر جای صفحه چرخش اسپینر شروع و یا متوقف می‌شود.

سرنخ‌های زیر برای اجرایی کردن این کار ارائه شده‌اند:

  • یک دستگیره رویداد click به عناصر ماوس اضافه می‌شود که شامل <body> سند است. اگر می‌خواهید ناحیه قابل کلیک را به بیشترین حد ممکن برسانید، قرار دادن آن در عنصر <body> مناسب خواهد بود. به این ترتیب این رویداد در فرزندان آن عنصر نیز اجرا می‌شود.
  • می‌توانید متغیر ردگیری را نیز اضافه کنید تا ببینید آیا اسپینر می‌چرخد یا نه، اگر چنین بود فریم را پاک کنید و در غیر این صورت آن را مجدداً فراخوانی کنید.

نکته: تلاش کنید تمرین فوق را خودتان اجرا کنید، اما اگر فکر می‌کنید موفق نخواهید بود می‌توانید کد کامل را در سورس زیر ملاحظه کنید:

محدودسازی انیمیشن ()requestAnimationFrame

یکی از مشکلات ()requestAnimationFrame این است که نمی‌توانید نرخ فریم خود را تعیین کنید. البته این وضعیت در اغلب موارد مشکلی محسوب نمی‌شود، چون به طور کلی می‌خواهیم انیمیشن تا حد امکان روان اجرا شود، اما اگر بخواهید یک انیمیشن 8 بیتی به سبک قدیم بسازید، متوجه خواهید شد که این مسئله مشکل‌زا است.

برای نمونه این وضعیت در انیمیشن زیر یک مشکل محسوب می‌شود:

در این مثال باید هم موقعیت کاراکتر را روی صفحه انیمیت کنیم و هم تصویر sprite دیده شود. تنها 6 فریم در انیمیشن sprite وجود دارد. اگر بخواهیم یک فریم sprite متفاوت را در هر فریم که روی صفحه نمایش می‌یابد با استفاده از ()requestAnimationFrame نمایش دهیم، کاراکتر مربوطه اندام‌های خود را چنان به سرعت جابجا می‌کند که انیمیشن ظاهر عجیبی پیدا می‌کند. از این رو چرخه‌های تصویر را با استفاده از کد زیر محدود می‌کنیم:

بنابراین ما صرفاً یک sprite را هر 13 فریم یک بار انیمیت می‌کنیم. بدین ترتیب در واقع نرخ فریم به 6.5 fps کاهش پیدا می‌کند چون posX (موقعیت کاراکتر روی صفحه) را در هر فریم به‌روزرسانی می‌کنیم:

این کدی است که شیوه به‌روزرسانی موقعیت را در هر فریم انیمیشن نمایش می‌دهد. متدی که شما برای محدودسازی انیمیشن استفاده می‌کنید به کد خاص آن مربوط خواهد بود. برای نمونه در مثال اسپینر می‌توانستیم آن را صرفاً با افزایش rotateCount به میزان یک فریم به جای دو فریم، طوری طراحی کنیم که کندتر به نظر برسد.

یادگیری عملی: بازی واکنش

در بخش نهایی این مقاله یک بازی واکنشی 2 نفره ایجاد می‌کنیم. در این بازی دو بازیکن داریم که یکی از آن‌ها بازی را با استفاده از کلید A کنترل می‌کند و دیگری بازی را با کلید L کنترل خواهد کرد.

زمانی که دکمه Start فشرده شود، یک اسپینر مانند آن که قبلاً دیدیم، به صورت تصادفی برای مدت زمان تصادفی بین 5 تا 10 ثانیه نمایش می‌یابد. پس از طی شدن این زمان پیامی ظاهر می‌شود که عبارت «!!PLAYERS GO» را نمایش می‌دهد. زمانی که این اتفاق بیفتد، بازیکن نخست باید دکمه کنترل خود را در بازی فشار دهد تا برنده شود.

قبل از هر چیز فایل اولیه این اپلیکیشن بازی را روی سیستم خود کپی کنید:

این فایل شامل ساختار کامل HTML و استایل‌دهی CSS است که یک صفحه بازی در اختیار ما قرار می‌دهد و اطلاعات دو بازیکن روی آن نمایش یافته‌اند. اما زمانی که اسپینر به چرخش درمی‌آید، پاراگراف نتایج روی پاراگراف دیگر نمایش پیدا می‌کند. اینک باید کد جاوا اسکریپت را بنویسیم.

درون عنصر <script> روی صفحه، کار خود را با اضافه کردن خطوط کد زیر برای تعریف برخی ثابت‌ها و متغیرها که در ادامه کد نیاز خواهیم داشت آغاز می‌کنیم:

در ادامه به ترتیب کارهای زیر را انجام دهید:

  1. یک ارجاع به اسپینر بسازید به طوری که بتوان آن را انیمیت کرد.
  2. یک ارجاع به عنصر <div> بسازید که شامل اسپینر باشد و برای نمایش یا مخفی کردن آن استفاده می‌شود.
  3. یک شمارنده چرخش بسازید که میزان چرخشی که می‌خواهیم اسپینر در هر فریم از انیمیشن داشته باشد نمایش می‌دهد.
  4. یک زمان آغاز null ایجاد کنید که در زمان شروع چرخش اسپینر، شروع به کار خواهد کرد.
  5. یک متغیر مقداردهی نشده در ادامه فراخوانی ()requestAnimationFrame را که اقدام به انیمیت اسپینر می‌کند ذخیره می‌سازد.
  6.  یک ارجاع به دکمه شروع بسازید.
  7. یک ارجاع به پاراگراف نتایج بسازید.

سپس زیر خطوط قبلی کد، تابع زیر را اضافه کنید. این تابع صرفاً ورودی‌های عددی را می‌گیرد و یک عدد تصادفی بین آن دو بازگشت می‌دهد. ما باید یک بازه timeout تصادفی در ادامه تولید کنیم:

سپس تابع ()draw را اضافه می‌کنیم که اسپینر را انیمیت می‌کند. این دقیقاً همان نسخه‌ای است که در مثال اسپینر قبلی دیدیم:

اکنون زمان آن رسیده است که حالت آغازین اپلیکیشن را در زمان بارگذاری اولیه صفحه تنظیم کنیم. خطوط کد زیر را اضافه کنید که به سادگی پاراگراف نتایج و کانتینر اسپینر را با استفاده از ;display: none پنهان می‌سازد:

ما همچنین یک تابع ()reset تعریف می‌کنیم که اپلیکیشن را به حالت ابتدایی تنظیم می‌کند و نیازمند شروع مجدد بازی است. کد زیر را به انتهای کدهای موجود اضافه کنید:

بدین ترتیب مراحل آماده‌سازی به پایان می‌رسد. در ادامه کاری می‌کنیم که گیم قابلیت بازی کردن پیدا کند. بلوک کد زیر را اضافه کنید. تابع ()start تابع ()draw را فراخوانی می‌کند که شروع به چرخاندن اسپینر کرده و آن را در UI نمایش می‌دهد و دکمه Start را پنهان می‌کند تا کاربر با کلیک کردن مجدد روی آن چندین نسخه را همزمان اجرا نکند. همچنین تابع فوق یک فراخوانی ()setTimeout را اجرا می‌کند که به نوبه خود تابع ()setEndgame را پس از مدت زمانی تصادفی بین 5 تا 10 ثانیه اجرا می‌کند. ضمناً یک شنونده رویداد نیز به دکمه خود اضافه می‌کنیم تا در زمان کلیک شدن، تابع ()start را اجرا کند.

نکته: در این مثال دیدیم که ()setTimeout بدون ذخیره‌سازی مقدار بازگشتی فراخوانی شد. این وضعیت کار می‌کند و مشکلی ندارد چون لزومی به پاکسازی interval/timeout در هیچ زمانی وجود ندارد. اگر قصد چنین کاری را داشته باشید، باید شناسه بازگشتی را ذخیره کنید.

نتیجه نهایی کد قبلی این است که وقتی دکمه Start فشرده شود، اسپینر، نمایش می‌یابد و بازیکنان باید مدت زمانی تصادفی منتظر بمانند تا از آن‌ها خواسته شود دکمه‌شان را فشار دهند. این بخش آخر به وسیله تابع ()setEndgame مدیریت می‌شود که در ادامه تعریف می‌کنیم:

در این مرحله تابع زیر را به ادامه کد خود اضافه کنید:

مراحل کار به صورت زیر است:

  • ابتدا انیمیشن اسپینر را با ()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 ثانیه استفاده می‌کنیم. چنان که قبلاً توضیح دادیم، این تابع بازی را به حالت اولیه‌اش ریست می‌کند و بازیکن‌ها می‌توانند بازی جدیدی را از ابتدا شروع کنند.

بدین ترتیب کار طراحی این تمرین به پایان می‌رسد. اگر هر گونه اشکالی در این تمرین داشتید می‌توانید از سورس کد کامل زیر کمک بگیرید:

سخن پایانی

به این ترتیب به پایان این بخش از سری مقالات آموزش جاوا اسکریپت می‌رسیم. همه موارد ضروری حلقه‌های ناهمگام و بازه‌ها در این مقاله توضیح داده شده‌اند. این متدها در موقعیت‌های مختلف کدنویسی به کار می‌آیند، اما مراقب باشید که از آن‌ها بیش از حد استفاده نکنید. چون این کدها همچنان در نخ اصلی اجرا می‌شوند و callback-های سنگین و شدید به خصوص آن‌هایی که مستلزم دستکاری DOM هستند معمولاً اگر مراقب نباشید موجب کند شدن صفحه می‌شوند.

برای مطالعه قسمت بعدی این مجموعه مطلب آموزشی می‌توانید روی لینک زیر کلیک کنید:

اگر این مطلب برای شما مفید بوده است، آمورش های زیر نیز به شما پیشنهاد می‌شوند:

==

«میثم لطفی» دانش‌آموخته ریاضیات و شیفته فناوری به خصوص در حوزه رایانه است. وی در حال حاضر علاوه بر پیگیری علاقه‌مندی‌هایش در رشته‌های برنامه‌نویسی، کپی‌رایتینگ و محتوای چندرسانه‌ای، در زمینه نگارش مقالاتی با محوریت نرم‌افزار نیز با مجله فرادرس همکاری دارد.

آیا این مطلب برای شما مفید بود؟

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *