در این مطلب، به روش حل مساله n وزیر با الگوریتم پس گرد (Backtracking) پرداخته میشود. همچنین، کدهای پیادهسازی روش مذکور در زبانهای گوناگون، ارائه شدهاند. در بازی شطرنج، وزیر میتواند به میزان نامحدودی به طور افقی، عمودی و قطری حرکت کند. در مساله n وزیر، هدف آن است که n وزیر در یک صفحه شطرنج n×n به گونهای قرار بگیرند که هیچ یک زیر ضرب دیگری نباشد. اولین مساله n وزیر در سال ۱۸۴۸ و با عنوان مساله ۸ وزیر مطرح شد. در سال ۱۸۵۰، «فرانز نائوک» (Franz Nauck) اولین راهکار برای مساله ۸ وزیر را ارائه کرد. او، مساله 8 وزیر را به مساله n وزیر تعمیم داد که در واقع همان قرارگیری n وزیر در یک صفحه شطرنج n×n است، به صورتی که یکدیگر را تهدید نکنند. ریاضیدانان متعددی روی مساله ۸ وزیر و حالت تعمیم یافته آن یا همان n وزیر کار و سعی کردند آن را حل کنند که از این جمله میتوان به «کارل فریدریش گاوس» (Carl Friedrich Gauss) اشاره کرد. «اس گانتر» (S. Gunther) در سال ۱۸۷۴ یک روش قطعی برای حل این مساله ارائه کرد و «جیمز گلشیر» (James Whitbread Lee Glaisher) راهکار ارائه شده توسط گانتر را بهبود بخشید.
سرانجام و در سال ۱۹۷۲، «ادسخر ویبه دِیکسترا» (Edsger W. Dijkstra) الگوریتم «اول عمق پسگرد» (Depth-First Backtracking) را برای حل این مساله معرفی کرد. حل مساله ۸ وزیر به لحاظ محاسباتی بسیار پرهزینه است، زیرا تنها در یک صفحه 8×۸ برای هشت وزیر، ۴,۴۲۶,۱۶۵,۳۶۸ حالت ممکن قرارگیری در صفحه شطرنج وجود دارد که از میان آنها، تنها ۹۲ مورد راه حل مساله است. بنابراین، با افزایش مقدار n، پیچیدگی محاسباتی افزایش پیدا میکند. البته، روشهایی برای کاهش پیچیدگی محاسباتی جهت حل این مساله ارائه شده است؛ اما به طور کلی، مساله n وزیر از جمله مسائل «انپی» (Non Deterministic Polynomial | NP) در حوزه «هوش مصنوعی» (Artificial Intelligence) محسوب میشود.
شایان توجه است که در مساله n وزیر، برای n=2 و n=۳ پاسخی وجود ندارد. همچنین، الزاما تعداد راهکارهای موجود برای مساله با افزایش N افزایش پیدا نمیکنند. برای مثال، مساله ۶ وزیر، تعداد پاسخهای کمتری نسبت به حالت ۵ وزیر دارد. در ادامه، روش حل مساله n وزیر برای N=4 نمایش داده شده است؛ سپس، یک الگوریتم ساده برای حل آن معرفی و در نهایت، مساله ۴ وزیر با استفاده از «الگوریتم پَسگرد» (Backtracking Algorithm) حل شده است. پیادهسازی الگوریتم پسگرد برای حل مساله N وزیر با زبانهای برنامهنویسی C++/C، پایتون و جاوا انجام شده است.

خروجی مورد انتظار، یک ماتریس دودویی است که در آن ۱ برای محلهایی که وزیر در آن قرار گرفته و ۰ برای محلهای فاقد وزیر است. برای مثال، ماتریس خروجی زیر، برای مساله ۴ وزیر است که تصویر آن در بالا وجود دارد.
{ 0, 1, 0, 0}
{ 0, 0, 0, 1}
{ 1, 0, 0, 0}
{ 0, 0, 1, 0}الگوریتم ساده برای حل مساله n وزیر، همه پیکربندیهای ممکن برای وزیرها روی صفحه را تولید میکند و پیکربندی را چاپ میکند که محدودیتهای داده شده را ارضا میکند.
در الگوریتم «پَسگرد» (Backtracking)، ایده آن است که وزیرها یکی یکی در ستونهای متفاوت قرار بگیرند و کار از چپترین ستون آغاز میشود. هنگامی که یک وزیر در ستون قرار میگیرد، بررسی میشود که آیا وزیر جدید قرار داده شده با سایر وزیرهای قرار گرفته در صفحه تصادم دارد یا خیر. در ستون کنونی، اگر سطری پیدا شود که هیچ ضربی بین وزیر جدید و سایر وزیرها وجود نداشته باشد، وزیر در آنجا قرار داده میشود. در غیر این صورت، پسگرد اتفاق میافتد و false بازگردانده میشود. الگوریتم پَسگرد برای حل مساله n وزیر، به صورت زیر است:
0 0 1 0 1 0 0 0 0 0 0 1 0 1 0 0
در این مقاله از سری مقالات آموزش سوئیفت بر روی مبحث نوشتن تست متمرکز خواهیم بود. شاید فکر کنید نوشتن تست یک کار اختیاری است و هیچ منطقی را اجرا نمیکند که به همراه اپلیکیشن عرضه شود. اگر واقعاً این گونه فکر میکنید، قطعاً در مصاحبه استخدامی خود مردود خواهید شد. با ما همراه باشید تا دلیل این مسئله را بازگو کنیم. برای مطالعه بخش قبلی این مجموعه مطلب آموزشی میتوانید به لینک زیر رجوع کنید:
تست کردن مهم است و ارتباط تنگاتنگی با رویکرد TDD دارد. TDD اختصاری برای عبارت «Test-driven Development» (توسعه تست-محور) است. توسعه تست-محور یک روش رایج برای نوشتن اپلیکیشن است و بهخاطرسپاری این فرمول نیز آسان است.
روشهای دیگری نیز برای تست کردن وجود دارند. میتوانید از گزارههای پرینت ساده برای نمایش نتایج قبل و بعد از اجرای کد استفاده کنید که روش خوبی برای تست کردن است. همچنین میتوانید اپلیکیشن خود را اجرا کنید تا مطمئن شوید که هیچ چیزی خراب نمیشود که در واقع کمترین حالت مورد نیاز برای تست است و احتمالاً به هر حال آن را اجرا خواهید کرد.
ما در این مقاله صرفاً به پوشش روش TDD میپردازیم. پیش از آن که کار خود را آغاز کنیم باید با دو اصطلاح جدید آشنا شوید: «Test Ratio» (نسبت تست) و «Code Coverage» (پوشش کد).
نسبت تست به نسبت تعداد خطوط کد به خطوط تستهای نوشتهشده گفته میشود. افراد زیادی هستند که میگویند هر 1 خط کد باید با 3 خط تست شود، یعنی این نسبت باید 1:3 باشد.
پوشش کد یعنی چه میزان از کد برحسب درصد تست شده است. IDE-های زیادی به نمایش پوشش کد میپردازند که کدبیس را برحسب وضعیت تست نمایش میدهد. رنگ سبز به معنی وجود تست برای کد و رنگ قرمز به معنی عدم وجود تست است.
برخی افراد بر این باورند که به صورت پیشفرض همه کدها باید تست شوند. اما شاید این دیدگاه چندان صحیح نباشد، چون لزومی وجود ندارد که تابعهای موجود در سوئیفت و دیگر کتابخانهها که خودشان شامل تست هستند مجدداً تست شوند. برای نمونه لازم نیست برای یک گزاره print یا دیگر متدهای استاتیک مانند ()Date.init تست نوشت. در واقع صرفاً لازم است که کد خودتان را تست کنید. تنها استثنا در این مورد کدهای افراد دیگری است که تست نداشته باشند.
نحوه نوشتن تست چیزی شبیه زیر است:
کد فوق را خط به خط بررسی میکنیم. import XCTest اقدام به ایمپورت کردن کتابخانه ارائه شده از سوی اپل برای تست کردن میکند. testable import ViewController @testable یک خصوصیت است که دامنه دسترسی این ماژول را افزایش میدهد. در واقع این دستور سطح دسترسی را از internal یا private به open تغیر میدهد اما تنها برای تستهای لوکال کار میکند. دستور import ViewController گزاره ایمپورتی است که شامل کلاس مورد نظر برای تست است.
{ … } class ViewControllerTests: XCTestCase کلاسی تعریف میکند که همه کارکردهای خود را از XCTestCase به ارث میبرد. اگر از هرکدام از مشخصهها استفاده کنید، چه کلاس دیگر باشد و چه برخی از ثابتها یا متغیرهایی که غالباً در کاربردهای تست استفاده میشود به جای Testing properties here// آن مشخصهها را بنویسید.
()override func setUp را میتوان به عنوان ()viewDidLoad برای حالتهای تست تصور کرد. زمانی که شروع به تست کردن میکنید، این کد کلاسها را ایجاد میکند یا از متغیرها وهله میسازد.
()override func tearDown معادل {} deinit در کنترلرهای نما است. هر چیزی را که ممکن است موجب ایجاد نشت حافظه شود پاک کنید، Timer مثال خوبی از آن چیزی است که باید حذف شود.
()func test_multiplyByTwoReturnsFour یکی از قراردادهای نامگذاری فراوانی است که وجود دارد، اما این نام گذاری مقصود تست را مشخص میکند. موارد تست را همواره با عبارت test آغاز کنید. استفاده از _ اختیاری است، اما به افزایش خوانایی کمک میکند. در ادامه multiplyByTwo آن چیزی است که قرار است انجام دهیم و ReturnsFour آن چیزی است که انتظار داریم دریافت کنیم. اگر موارد تست را به این ترتیب بنویسید همواره میدانید که هر مورد تست برای چه چیزی استفاده میشود و چه نتیجهای از آن انتظار میرود. اما اگر در نهایت صرفاً اعداد فرد و گرد شده بازگشت یابند چطور؟ بدین ترتیب میتوان مورد تستی مانند {} func test_getOddFromMultiplyByTwo_ReturnsFive نوشت.
در نهایت دستور (XCTAssertEqual(value، value را میبینیم که مورد تست واقعی است که برابر بودن هر دو مقدار را تست میکند. XCTAssert یک پیشوند رایج است و از این رو اگر شروع به نوشتن XCT بکنید امکان تکمیل خودکار در Xcode تعدادی از متدهایی که میتوانید استفاده کنید را به شما پیشنهاد میکند. در این حالت اگر هر دو مقدار برابر باشند تست پاس میشود و در غیر این صورت ناموفق خواهد بود.
برخی تستهای رایج دیگر شامل XCTAssertNotEqual ،XCTAssertNil و XCTAssertNotNil هستند.
تست کردن تنها به بررسی این که اشیای مختلف باید چگونه باشند محدود نمیشود بلکه میتوان به اندازهگیری عملکرد نیز پرداخت. این نوع از تست یکی از مفیدترین تستها است زیرا پس از این که مطمئن شدیم کار درستی انجام میدهیم باید اطمینان پیدا کنیم که آن را به طرز صحیحی نیز انجام میدهیم. بدین ترتیب میتوانیم به مرور کد خود را بهبود بخشیم.
برخلاف تستهای تأییدی ما، نیازی نیست که بخش بازگشتی مورد انتظار را در اعلان متد بگنجانیم. به جای آن کافی است آن را با بلوک { }test_functionNamePerformance(). measure عوض کنیم که باید صرفاً شامل کدی باشد که میخواهیم اندازهگیری کنیم. اگر قرار باشد یک متغیر به (:getFactorial(of ارسال کنیم، در این صورت آن را خارج از بلوک اندازهگیری وهلهسازی میکنیم، مگر این که بخواهیم آن را در اندازهگیریهای خود بگنجانیم.
(getFactorial(of: 45 =_ به فراخوانی متد getFactorial با ورودی 45 میپردازد. از آنجا که نیازی به ذخیرهسازی مقدار نداریم از =_ استفاده میکنیم که نوعی جابجایی نتایج به /dev/null محسوب میشود. برای ما مهم نیست که این مقدار چیست و در اینجا به آن اهمیتی نمیدهیم.
آنچه در اینجا برای ما مهم است، این است که تابع فاکتوریل چه قدر طول میکشد تا کار خودش را اجرا بکند. زمانی که این کد را اجرا کنید موارد تست 10 بار اجرا میشوند.
در اجرای نخست این کد تابع را در طی 0.00000263 اجرا میکند. این زمان کاملاً سریع است اما در طی این مدت بهینهسازیهایی رخ میدهند. در ادامه اجراهای بعد را میبینیم.
در اجرای دوم، ما این تابع را 39 درصد بهتر اجرا کردهایم، اما هیچ چیز تغییر نیافته است. دلیل این امر آن است که بهینهسازیها قبلاً صورت گرفتهاند و صرفاً از آنها مجدداً استفاده کردهایم. این جا مکان خوبی برای تنظیم مبدأ جدید است بنابراین ویرایش را کلیک میکنیم و مبدأ جدید را پذیرفته و آن را ذخیره میکنیم. در اجراهای بعدی به ترتیب 6% عملکرد بدتر، 0% بهتر، 2% بهتر، و 7% بهتر بدون تغییر دادن هیچ خطی از کد به دست آمدند. بنابراین مبدأ خوبی برای ما محسوب میشود. از این جا میتوانیم تغییرات خود را ایجاد کرده و سپس اندازهگیریها را تست کنیم تا ببینیم آیا تغییرات ما موجب عملکرد بهتر یا بدتر میشوند.
با این که راهنمای صریحی در مورد آن چه بهینهسازی خوب شمرده میشود وجود ندارد اما تصور ما این است هر تغییری که موجب بهبود در طی 10 اندازهگیری (100 اجرا) شود مناسب است. اگر کدی در طی چند اجرای نخست بهبود مناسبی نشان دهد بهتر است آن تغییر را حفظ کنید.
زمانی که در مورد تست کردن Unit Testing صحبت میکنیم، در واقع همان فرایندی است که در بخش فوق توضیح دادیم. همچنان یک حالت تست کردن عمومی نیز وجود دارد که به تست کارکرد کلی عملکرد اپلیکیشن چنان که انتظار میرود میپردازید. UI Testing نوع دیگری از تست کردن است که بخش UI را در برمیگیرد، اما از آنجا که این راهنما صرفاً در مورد مفاهیم مقدماتی تست کردن است بررسی آن خارج از دامنه این مقاله خواهد بود. در نهایت باید اشاره کنیم که یک تست پذیرش کاربر (UAT) نیز وجود دارد که برای اطمینان یافتن از این که کاربر از امکانات اپلیکیشن راضی است اجرا میشود. این تست عموماً از طریق تیمهای پرسش و پاسخ (QA) اجرا میشود و کسبوکار یا مخاطبان منتخب از کاربران نهایی را شامل میشود. این تست امکان اجرای سناریوهای بیشتری در اپلیکیشن را میدهد که برای تست کردن بیشتر استفاده میشوند و به سؤالاتی در مورد شیوه استفاده از اپلیکیشن و زمان عرضه نهایی آن پاسخ میدهد.
ما در این مقاله با مفاهیم مقدماتی تست کردن آشنا شدیم و دیدیم که چگونه میتواند به نوشتن کدهای بهتر کمک کند، چه اهمیتی در چرخه توسعه دارد و چگونه عملکرد کد را اندازهگیری میکند. تستهای بیشتر شامل استفاده از ابزارهای Xcode است که میتوان از آنها برای اندازهگیری حافظه، CPU و استفاده از دیسک بهره گرفت، اما اینها جزء مباحث پیشرفته هستند. در بخش بعدی در مورد معماری Model View Controller صحبت میکنیم.
معماری مدل، نما-کنترلر یا به اختصار MVC به صورت گستردهای برای یادگیری آموزش کدنویسی به افراد مبتدی استفاده میشود. این سادهترین روش برای یادگیری شیوه استفاده از چندین فایل در یک پروژه است. با این که روشهای دیگری نیز وجود دارند که میتوان استفاده کرد، اما این سادهترین نوع معماری محسوب میشود. زمانی که آماده انتقال از MVC باشید تقریباً به طور طبیعی شروع به نوشتن یک سبک معماری متفاوت برحسب نیازهای اپلیکیشن خود میکنید. برای مطالعه بخش بعدی (پایانی) به اینک زیر مراجعه کنید:
منبع: فرادرس
چند قلاب ریاکت وجود دارند که در این نوشته به بررسی آنها میپردازیم. همچنین یک قلاب سفارشی میسازیم که تنها از قلاب useState برای اعتبار سنجی فرم استفاده میکند. اگر میخواهید مثال عملی آن را ببینید به این صفحه (+) مراجعه کنید. ایده کار این است که یک قلاب سفارشی ایجاد کنیم که برخی دادههای اولیه، اعتبارسنجی و اعتبارسنجی کنندهها را دریافت کند. کد نهایی این قلاب به صورت زیر خواهد بود. در ادامه طرز کار آن و همچنین شیوه استفاده از قلاب های React برای اعتبارسنجی یک فرم را بررسی میکنیم.
به طور کلی این قلاب یک آرایه با 2 بخش «حالت» (State) و 3 تابع بازگشت میدهد. در ادامه هر یک از این موارد را بررسی میکنیم.
متغیر فرم، حالت را به همراه تابع nChange و onClick برای همه بخشهای حالت نمایش میدهد و این بدان معنی است که دادههای اولیه فرم به صورت زیر در اختیار ما است:
متغیر فرم که از سوی قلاب بازگشت مییابد به صورت زیر خواهد بود:
بخش اعتبارسنجی پاسخهای اعتبارسنجی حاصل از valida-js را نگهداری میکند.
3 تابع دیگر به صورت زیر هستند:
ابتدا قلاب را از npm ایمپورت میکنیم و سپس آنها را درون تابع کامپوننت قرار میدهیم.
قلاب به نام useValidatedForm را میتوان دستکم در دو پارامتر مورد استفاده قرار داد که یکی دادههای اولیه برای فرم است (که باید طرحبندی کامل دادهها باشد) و پارامتر دوم آرایه قواعد اعتبارسنجی است. این قواعد اعتبارسنجی برای valida-js استفاده خواهد شد.
اینک مقدار بازگشتی از قلاب و شیوه مقداردهی اولیه آن را میدانیم. در ادامه فرم کوچکی را به وسیله آن مینویسیم:
بخشهای مهم به شرح زیر هستند:
در ابتدا بررسی میکنیم که در صورتی که مشخصهای خطا داشته باشد، چگونه باید آن را بخوانیم. شاید بهتر بود که یک تابع برای انجام این کار مینوشتیم، اما فعلاً از همین منطق ساده استفاده میکنیم. اگر آرایهای از خطاهای یک مشخصه، طولی بلندتر از 0 داشته باشید به این معنی است که خطایی رخ داده است و در غیر این صورت فرض ما این است که خطایی وجود ندارد.
بخش مهم دیگر به صورت زیر است:
جایی که شیء ورودی firstName را افراز میکنیم به این معنی است که props را به ورودی value، دستگیره onClick و دستگیره onChange میفرستیم. به این ترتیب قصد داریم متادیتای مشخصه را بهروزرسانی کنیم. همچنین مقدار ورودی را زمانی که کاربر در ورودی مینویسد بهروز کرده و اعتبارسنجی در مورد مشخصه خاص فرم را نیز بهروزرسانی میکنیم. از این رو validation.errors.firstName نیز در این حالت بهروزرسانی میشود.
یکی از موضوعات شگفتانگیز در این مورد آن است که اینک میتوانیم کامپوننتهای کنترل را بسازیم و validation.property، form.property و منطق دقیق را برای قرار دادن خطا و کلاسها در آنجا ارسال کنیم.
در این مثال تنها از یکی از قلابهای موجود روی React یعنی useState استفاده کردیم، اما شما میتوانید به بررسی قلابهای دیگر و روش استفاده از آنها نیز بپردازید.
منبع: فرادرس
چند قلاب ریاکت وجود دارند که در این نوشته به بررسی آنها میپردازیم. همچنین یک قلاب سفارشی میسازیم که تنها از قلاب useState برای اعتبار سنجی فرم استفاده میکند. اگر میخواهید مثال عملی آن را ببینید به این صفحه (+) مراجعه کنید. ایده کار این است که یک قلاب سفارشی ایجاد کنیم که برخی دادههای اولیه، اعتبارسنجی و اعتبارسنجی کنندهها را دریافت کند. کد نهایی این قلاب به صورت زیر خواهد بود. در ادامه طرز کار آن و همچنین شیوه استفاده از قلاب های React برای اعتبارسنجی یک فرم را بررسی میکنیم.
به طور کلی این قلاب یک آرایه با 2 بخش «حالت» (State) و 3 تابع بازگشت میدهد. در ادامه هر یک از این موارد را بررسی میکنیم.
متغیر فرم، حالت را به همراه تابع nChange و onClick برای همه بخشهای حالت نمایش میدهد و این بدان معنی است که دادههای اولیه فرم به صورت زیر در اختیار ما است:
متغیر فرم که از سوی قلاب بازگشت مییابد به صورت زیر خواهد بود:
بخش اعتبارسنجی پاسخهای اعتبارسنجی حاصل از valida-js را نگهداری میکند.
3 تابع دیگر به صورت زیر هستند:
ابتدا قلاب را از npm ایمپورت میکنیم و سپس آنها را درون تابع کامپوننت قرار میدهیم.
قلاب به نام useValidatedForm را میتوان دستکم در دو پارامتر مورد استفاده قرار داد که یکی دادههای اولیه برای فرم است (که باید طرحبندی کامل دادهها باشد) و پارامتر دوم آرایه قواعد اعتبارسنجی است. این قواعد اعتبارسنجی برای valida-js استفاده خواهد شد.
اینک مقدار بازگشتی از قلاب و شیوه مقداردهی اولیه آن را میدانیم. در ادامه فرم کوچکی را به وسیله آن مینویسیم:
بخشهای مهم به شرح زیر هستند:
در ابتدا بررسی میکنیم که در صورتی که مشخصهای خطا داشته باشد، چگونه باید آن را بخوانیم. شاید بهتر بود که یک تابع برای انجام این کار مینوشتیم، اما فعلاً از همین منطق ساده استفاده میکنیم. اگر آرایهای از خطاهای یک مشخصه، طولی بلندتر از 0 داشته باشید به این معنی است که خطایی رخ داده است و در غیر این صورت فرض ما این است که خطایی وجود ندارد.
بخش مهم دیگر به صورت زیر است:
جایی که شیء ورودی firstName را افراز میکنیم به این معنی است که props را به ورودی value، دستگیره onClick و دستگیره onChange میفرستیم. به این ترتیب قصد داریم متادیتای مشخصه را بهروزرسانی کنیم. همچنین مقدار ورودی را زمانی که کاربر در ورودی مینویسد بهروز کرده و اعتبارسنجی در مورد مشخصه خاص فرم را نیز بهروزرسانی میکنیم. از این رو validation.errors.firstName نیز در این حالت بهروزرسانی میشود.
یکی از موضوعات شگفتانگیز در این مورد آن است که اینک میتوانیم کامپوننتهای کنترل را بسازیم و validation.property، form.property و منطق دقیق را برای قرار دادن خطا و کلاسها در آنجا ارسال کنیم.
در این مثال تنها از یکی از قلابهای موجود روی React یعنی useState استفاده کردیم، اما شما میتوانید به بررسی قلابهای دیگر و روش استفاده از آنها نیز بپردازید.
منبع: فرادرس
حلقه Event یکی از مهمترین جنبههای جاوا اسکریپت است که باید درک شود. در این بخش از مقالات آموزش Node.js جزییات دقیق طرز کار جاوا اسکریپت با «نخ» (Thread) منفرد و شیوه مدیریت تابعهای ناهمگام را مورد بررسی قرار میدهیم. برای مطالعه بخش قبلی این سری مقالات آموزشی به لینک زیر مراجعه کنید:
بسیاری از افرادی که سالها تجربه برنامهنویسی جاوا اسکریپت دارند همچنان از طرز کار دقیق بسیاری از موارد در پشت پرده خبر ندارند. البته این که از جزییات دقیق این مفاهیم خبر نداشته باشید اشکالی ندارد، اما به طور معمول بهتر است با طرز کار کلی آنها آشنا باشید و ضمناً این احتمال وجود دارد که در این مرحله از آموزش Node.js در این خصوص کنجکاو شده باشید.
کد جاوا اسکریپت به صورت تک نخی اجرا میشود. بنابراین در هر زمان تنها یک اتفاق در حال وقوع است. این محدودیتی است که عملاً مفید است، چون سهولت زیادی در امر برنامهنویسی ایجاد میکند و نیازی به نگرانی در مورد مشکلات «همزمانی» (concurrency) وجود ندارد. شما باید صرفاً به روش نوشتن کد توجه داشته باشید و از هر چیزی که میتواند نخ را مسدود سازد مانند فراخوانیهای همگام شبکه یا حلقههای بینهایت جلوگیری کنید.
به طور کلی، در اغلب مرورگرها یک حلقه Event روی هر برگه مرورگر وجود دارد که موجب میشود هر پردازشی مجزا باشد و از مسدودسازی کل مرورگر در نتیجه یک پردازش سنگین یا حلقه بینهایت جلوگیری شود. این محیط چندین حلقه Event همزمان را مدیریت میکند تا برای نمونه فراخوانیهای API را مدیریت کند. بدین ترتیب Web Workers در حلقه رویداد خودشان اجرا میشوند. شما به صورت عمده باید در مورد این نگران باشید که کد روی یک حلقه رویداد منفرد اجرا خواهد شد و در هنگام نوشتن کد این ذهنیت جلوگیری از مسدودسازی را مداوماً داشته باشید.
هر کد جاوا اسکریپتی که بازگرداندن کنترل به حلقه event را بیش از حد طول بدهد، موجب مسدود شدن اجرای کد جاوا اسکریپت در صفحه خواهد شد و حتی ممکن است نخ UI را مسدود کند. در این حالت کاربر دیگر نمیتواند روی چیزی کلیک کند و یا کارهایی مانند اسکرول و نظایر آن انجام دهد.
تقریباً همه کارهای ابتدایی I/O در جاوا اسکریپت غیر مسدودکننده هستند. بنابراین درخواستهای شبکه، عملیات فایل سیستم Node.js و مواردی از این دست همگی غیر مسدودکننده هستند. مسدودکننده بودن یک استثنا است و به همین دلیل جاوا اسکریپت به طور عمده بر مبنای callback-ها کار میکند. البته در نسخههای اخیر تمرکز بیشتر روی promises و async/await انتقال یافته است.
پشته فراخوانی یک صف LIFO به معنی «ورودی آخر، خروجی اول» (Last In ،First Out) است. حلقه Event به طور پیوسته پشته فراخوانی را بررسی میکند تا ببیند آیا هیچ تابعی نیاز به اجرا دارد یا نه. در زمانی که چنین نیازی باشد، هر فراخوانی تابعی را که مییابد به پشته فراخوانی اضافه میکند تا به ترتیب اجرا شوند. همه شما «رد پشته خطا» (Error Stack Trace) را میشناسید و آن را در دیباگر یا در کنسول مرورگر دیدهاید.
مرورگر نامهای تابعها را در پشته فراخوانی بررسی میکند تا مطلع شود که کدام تابع فراوانی جاری را آغاز کرده است:

به مثال زیر توجه کنید:
این کد مقدار زیر را نمایش میدهد:
Foobarbaz
که مطابق انتظار است. زمانی که این کد اجرا میشود ابتدا ()foo فراخوانی میشود. درون ()foo ابتدا ()bar را فراخوانی میکنیم و سپس ()baz فراخوانی میشود. در این مرحله پشته فراخوانی مانند زیر است:

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

تا این که پشته فراخوانی خالی شود.
مثال فوق معمولی به نظر میرسد و نکته خاصی ندارد: جاوا اسکریپت مواردی که باید اجرا شوند را مییابد و آنها را به ترتیب اجرا میکند. در ادامه با روش به تعویض انداختن (defer) یک تابع تا زمان خالی شدن پشته آشنا میشویم. کاربرد دستور زیر برای فراخوانی یک تابع است:
اما هر بار که تابع دیگری در کد اجرا شود، این دستور نیز اجرا خواهد شد. مثال زیر را در نظر بگیرید:
شاید شگفتزده شوید که کد فوق عبارت زیر را در خروجی نمایش میدهد:
Foobazbar
زمانی که این کد اجرا شود، ابتدا ()foo فراخوانی میشود. درون ()foo ابتدا setTimeout فراخوانی میشود و bar به عنوان یک آرگومان ارسال میشود. ما آن را طوری تنظیم میکنیم تا حد امکان به سرعت اجرا شود و مقدار 0 به عنوان تایمر ارسال میشود سپس ()baz را فراخوانی میکنیم. در این نقطه پشته فراخوانی مانند زیر خواهد بود:

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

چرا چنین اتفاقی رخ میدهد؟ در بخش بعدی به این سؤال پاسخ میدهیم.
زمانی که ()setTimeout فراخوانی میشود، مرورگر یا Node.js تایمر را آغاز میکند. زمانی که تایمر منقضی شود، در این حالت از آنجا که مقدار 0 به عنوان timeout تعیین شده است، تابع callback در «صف پیام» (Message Queue) قرار میگیرد.
صف پیام جایی است که رویدادهای آغاز شده از سمت کاربر مانند رویدادهای کلیک و ضربههای کیبورد و یا واکشی پاسخها از صف پیش از آن که کد فرصت واکنش به آنها را داشته باشد صفبندی میشوند. رویدادهای DOM مانند onLoad نیز چنین خصوصیتی دارند.
این حلقه به پشته فراخوانی اولویت میدهد. این حلقه ابتدا همه چیز که در پشته فراخوانی بیاید پردازش میکند و زمانی که چیزی باقی نماند، اقدام به انتخاب موارد موجود در صف پیام میکند.
بدین ترتیب لازم نیست برای تابعهایی مانند setTimeout منتظر بمانیم یا این که صبر کنیم واکشی یا دیگر امور به پایان برسند، زیرا از سوی مرورگر ارائه شدهاند و روی نخهای خود زنده هستند. برای نمونه اگر مقدار timeout را با استفاده از دستور setTimeout روی 2 ثانیه تنظیم کرده باشید، لازم نیست 2 ثانیه منتظر بمانید چون انتظار در هر جایی رخ میدهد.
استاندارد ECMAScript 2015 مفهوم «صف کار» (Job Queue) را معرفی کرده است که از سوی Pomise-ها مورد استفاده قرار میگیرد و روشی برای اجرای نتیجه یک تابع async به محض امکان است و دیگر آن را در انتهای پشته فراخوانی قرار نمیدهیم. بدین ترتیب Promise-هایی که پیش از اتمام تابع جاری خاتمه یابند، درست پس از تابع جاری اجرا خواهند شد.
برای مقایسه میتوانید یک قطار هوایی شهر بازی را در نظر بگیرید. صف پیام شما را در پشت همه افرادی که قبل از شما در صف جای گرفتهاند قرار میدهد، در حالی که صف کار یک بلیت سریعالسیر است که اجازه میدهد درست پس از اتمام یک دور، مجدداً بیدرنگ سوار قطار هوایی شوید.
مثال:
کد فوق عبارت زیر را نمایش میدهد:
foobazshould be right after foo، before barbar
این تفاوت بزرگی است که بین Promise-ها (و البته async/await که بر مبنای Promise ساخته شده) با تابعهای ساده قدیمی ناهمگام که از طریق ()setTimeout یا دیگر API-های پلتفرم اجرا میشدند وجود دارد.
زمانی که تلاش میکنید حلقه رویداد Node.js را درک کنید، یک بخش مهم آن ()process.nextTick است. این بخش با حلقه رویداد به روشی خاص تعامل پیدا میکند. هر بار که حلقه رویداد یک دور کامل میزند آن را یک tick مینامیم.
زمانی که یک تابع را به ()process.nextTick ارسال میکنیم به موتور مرورگر دستور میدهیم که تابع را در انتهای عملیات جاری و پیش از آغاز تیک بعدی حلقه رویداد احضار کند:
حلقه رویداد مشغول پردازش کردن کد تابع جاری است. زمانی که این عملیات پایان گیرد، موتور جاوا اسکریپت همه تابعهای ارسالی در فراخوانیهای nextTick که در طی اجرای این عملیات ارسال شدهاند را اجرا میکند. به این ترتیب به موتور جاوا اسکریپت اعلام میکنیم که یک تابع را به روشی ناهمگام (پس از اجرای تابع جاری) اما به سرعت و بدون صفبندی پردازش کند. فراخوانی کردن (setTimeout(() => {}، 0 موجب میشود که تابع در تیک بعدی و بسیار بعدتر از زمانی که از ()nextTick استفاده میکنیم اجرا شود. از ()nextTick زمانی استفاده کنید که میخواهید مطمئن شوید در تکرار بعدی حلقه رویداد، کد حتماً اجرا خواهد شد.
هنگامی که میخواهیم بخشی از کد را به صورت ناهمگام اجرا کنیم، اما این کار در سریعترین زمان ممکن صورت گیرد، یک گزینه استفاده از تابع ()setImmediate است که از سوی Node.js ارائه شده است:
هر تابعی که به صورت آرگومان ()setImmediate ارسال شود یک callback است که در تکرار بعدی حلقه رویداد اجرا خواهد شد. اینک شاید از خود بپرسید ()setImmediate چه تفاوتی با (setTimeout(() => {}، 0 و یا ()process.nextTick دارد؟ تابعی که به ()process.nextTick ارسال شود در تکرار بعدی حلقه رویداد و پس از پایان یافتن عملیات اجرا خواهد شد. این بدان معنی است که همواره پیش از ()setTimeout و ()setImmediate اجرا میشود. یک callback به نام ()setTimeout با تأخیر 0 میلیثانیه بسیار به ()setImmediate شباهت دارد. ترتیب اجرا به عوامل مختلفی وابسته خواهد بود، اما هر دوی آنها در تکرار بعدی حلقه رویداد اجرا خواهند شد.
زمانی که کد جاوا اسکریپت مینویسیم ممکن است بخواهیم اجرای یک تابع را به تأخیر بیندازیم. به این منظور میتوان از ()setTimeout و ()setInterval برای زمانبندی اجرای تابع در آینده استفاده کرد.
()setTimeout
هنگامی که کد جاوا اسکریپت مینویسیم، میتوانیم با استفاده از دستور ()setTimeout اجرای یک تابع را به تأخیر بیندازیم. میتوان یک تابع callback تعیین کرد که بعدتر اجرا شود و مقداری برای میزان این تأخیر در اجرا بر مبنای میلیثانیه تعیین کرد:
این ساختار یک تابع جدید تعریف میکند. شما میتوانید تابع دیگر را درون آن فراخوانی کنید یا این که نام یک تابع موجود را به آن ارسال و پارامترهای آن را تعیین کنید:
()setTimeout یک شناسه تایمر بازگشت میدهد. این شناسه عموماً استفادهای ندارد، اما میتوانید آن را ذخیره کنید و در صورتی که بخواهید اجرای این تابع زمانبندیشده را حذف کنید آن را پاک کنید:
اگر میزان تأخیر را برابر با 0 تعیین کنید، تابع callback در اولین فرصت ممکن، اما پس از اجرای تابع جاری اجرا خواهد شد:
کد فوق عبارت زیر را نمایش میدهد:
before after
این حالت به طور خاص در مواردی که میخواهیم از مسدود شدن CPI روی وظایف سنگین جلوگیری کنیم و اجازه دهیم تابعهای دیگر نیز در زمان اجرای یک محاسبه سنگین اجرا شوند مفید خواهد بود. این کار از طریق صفبندی تابعها در یک جدول زمانبندی ممکن خواهد بود. برخی مرورگرها (مانند IE و Edge) یک متد ()setImmediate را پیادهسازی کردهاند که دقیقاً همین کارکرد را انجام میدهد، اما استاندارد نیست و روی مرورگرهای دیگر وجود ندارد. اما این تابع که معرفی کردیم در Node.js یک استاندارد محسوب میشود.
()setInterval یک تابع مشابه ()setTimeout است و تنها یک تفاوت دارد. به جای اجرا کردن یکباره تابع callback این تابع آن را برای همیشه و در بازههای زمانی معین شده (بر حسب میلیثانیه) اجرا میکند:
تابع فوق هر 2 ثانیه یک بار اجرا میشود، مگر اینکه با استفاده از clearInterval و ارسال شناسه بازهای که در پی اجرای setInterval بازگشت مییابد از آن بخواهیم متوقف شود:
فراخوانی clearInterval درون تابع callback setInterval رویهای رایج است و بدین ترتیب میتوان در مورد اجرای مجدد یا توقف آن یک تصمیمگیری خودکار داشت. برای نمونه کد زیر کار دیگری را اجرا میکند، مگر اینکه App.somethingIWait دارای مقدار arrived باشد:
setTimeout یک تابع را هر n میلیثانیه یک بار اجرا میکند و هیچ ملاحظهای در مورد زمان پایان اجرای تابع ندارد. اگر یک تابع همواره زمان مشابهی را نیاز داشته باشد، این روش مناسب خواهد بود:

اما ممکن است تابع برای نمونه بسته به شرایط شبکه، برای اجرا به زمان متفاوتی نیاز داشته باشید:

و حتی ممکن است این زمان طولانی اجرا با تابع بعدی همپوشانی پیدا کند:

برای جلوگیری از این وضعیت میتوان یک setTimeout بازگشتی زمانبندی کرد تا زمانی که تابع callback به پایان میرسد فراخوانی شود:
برای دستیابی به این سناریو:

setTimeout و setInterval هر دو در Node.js از طریق ماژول Timers در اختیار ما قرار گرفتهاند. ()Node.js تابع setImmediate را نیز ارائه کرده است که معادل استفاده از دستور زیر است:
از این دستور به طور غالب برای کار با حلقه Event در Node.js استفاده میشود. بدین ترتیب به پایان بخش ششم این سری مقالههای آموزش Node.js میرسیم. در بخش بعدی در مورد برنامهنویسی ناهمگام و Callback-ها توضیح خواهیم داد. برای مطالعه بخش بعدی به لینک زیر مراجعه کنید: