فصل 11برنامهنویسی ناهمگام
چه کسی میتواند تا تهنشین شدن گلهای آب، شکیبا باشد؟ چهکسی میتواند بیحرکت بماند تا لحظهی درست عمل فرا برسد؟
بخش مرکزی یک کامپیوتر، بخشی که گامهای اجرای برنامهی ما را برمیدارد، پردازشگر نامیده می شود. برنامههایی که تا کنون دیدهایم از نوعی بوده اند که پردازشگر را تا وقتی که کارشان تمام شود، مشغول نگه می دارند. سرعت اجرا در چیزی مثل یک حلقه که با اعداد سر و کار دارد به میزان زیادی به سرعت پردازشگر ارتباط دارد.
اما خیلی از برنامهها، با چیزهایی غیر از پردازشگر تعامل دارند. به عنوان مثال، ممکن است با کامپیوتری در یک شبکه تعامل داشته باشند یا دادهها را از دیسک سخت بخوانند – که خیلی کندتر از گرفتن آن ها از حافظهی اصلی است.
زمانی که اتفاقاتی این چنینی می افتد، بی کار گذاشتن پردازشگر کار نادرستی است- ممکن است کارهای دیگری وجود داشته باشد که بتوان در آن حین انجام داد. این کار تا حدی توسط سیستم عامل مدیریت میشود که پردازشگر را بین برنامههای متعددی که در حال اجرا هستند بکار میگیرد. اما در مواقعی که میخواهیم یک برنامهی واحد بتواند در هنگام انتظار برای یک درخواست شبکه به اجرا و پیشرفت خود ادامه دهد، از سیستمعامل کمکی بر نمیآید.
ناهمگامی
در یک مدل برنامهنویسی همگام، همه چیز یک به یک اتفاق می افتد. زمانی که تابعی را فراخوانی میکنید که عهدهدار اجرای کاری طولانی است، تنها بعد از اتمام آن کار و برگرداندن نتیجهی تابع، کنترل به برنامه برمیگردد. در حین اجرای تابع، برنامهی شما متوقف خواهد ماند.
در مدل ناهمگام میتوان چندین کار را در یک زمان انجام داد. زمانی که کاری را شروع میکنید، برنامهی شما به اجرا ادامه خواهد داد. زمانی که آن کار تمام میشود، برنامه خبردار شده و به نتایج دست خواهد یافت (به عنوان مثال میتوان به خواندن اطلاعات از دیسک سخت اشاره کرد).
میتوان برنامهنویسی همگام و ناهمگام را با یک مثال کوچک مقایسه کرد: برنامهای که از دو منبع در شبکه، اطلاعاتی دریافت میکند و بعد نتایج را با هم ترکیب میکند.
در یک محیط همگام، جایی که درخواست فقط زمانی برمی گردد که کارش را تمام کرده باشد، آسان ترین روش انجام این کار ارسال درخواست ها یکی پس از دیگری است. مشکل این روش این است که درخواست دوم زمانی شروع میشود که درخواست اول تمام شده باشد. جمع زمانی که صرف میشود حداقل برابر است با مجموع زمان پاسخهای درخواست ها.
راه حل این مسئله، در یک سیستم همگام، استفاده از نخهای (threads) اضافی کنترل است. یک thread یک برنامهی دیگر است که در حال اجرا است که اجرای آن ممکن است توسط سیستم عامل بین برنامههای دیگر قرار گیرد – چون بیشتر کامپیوترهای مدرن دارای چندین پردازشگر هستند، چندین thread را میتوان در یک آن روی پردازشگرها اجرا کرد. یک thread دیگر میتواند درخواست دوم را شروع کند و سپس هر دوی thread ها منتظر نتیجهی درخواستشان می مانند که بعد از آن دوباره همگام شده و نتایج را باهم ترکیب می کنند.
در نمودار پیش رو، خطوط درشت نمایانگر زمانی است که برنامه سپری میکند تا در حالت نرمال اجرا شود، و خطوط باریک نشانگر زمانی است که برای پاسخ شبکه صرف میشود. در مدل همگام، زمانی که توسط شبکه گرفته میشود به عنوان بخشی از جدول زمانی برای thread داده شده محسوب میشود. در مدل ناهمگام، شروع یک عملیات مرتبط با شبکه، به طور مفهومی باعث ایجاد یک انشعاب در جدول زمانی میشود. برنامهای که این انشعاب را شروع کرده است به اجرای خود ادامه می دهد، و آن عملیات به موازات آن انجام میشود و وقتی پایان یافت برنامه را باخبر میکند.
راه دیگری که میتوان با آن تفاوت این دو را بیان کرد این است که در مدل همگام، انتظار برای پایان درخواستها به صورت ضمنی است در حالیکه در مدل ناهمگام صریح و تحت کنترل ما میباشد.
ناهمگامی مثل چاقوی دولبه است. برای برنامههایی که مناسب اجرای مستقیم خطی نیستند کار را ساده تر میکند اما در عین حال میتواند برای برنامههایی که به صورت مستقیم خطی اجرا میشوند نامناسب باشد. در ادامه این فصل با راههایی برای حل این ناهمگونی آشنا خواهیم شد.
هر دو پلتفرم مهم برنامهنویسی جاوااسکریپت – مرورگرها و <bdo>
Node.js</bdo>
– عملیاتی که ممکن است زمانگیر باشند را به صورت ناهمگام اجرا می کنند و از نخها (threads) استفاده نمی کنند. به دلیل اینکه برنامهنویسی روی thread ها کار سختی محسوب میشود (در این نوع برنامهنویسی درک کارکرد برنامه، به دلیل انجام چند کار در آن واحد بسیار سختتر میشود)، روش ناهمگام عموما چیز خوبی محسوب میشود.
فناوری کلاغها
خیلی از مردم میدانند که کلاغها پرندههایی بسیار باهوش هستند. آن ها می توانند از ابزار استفاده کنند، برای آینده برنامه ریزی کنند، چیزهایی را به خاطر بسپارند و حتی این موارد را با هم به اشتراک بگذارند.
چیزی که بیشتر مردم از آن آگاه نیستند این است که کلاغها توانایی های زیادی دارند که از دید ما مخفی می کنند. یکی از متخصصین مشهور (و کمی عجیب و غریب) کلاغها به من گفت که فناوری کلاغ خیلی از فناوری انسان عقب نیست و به زودی به انسان ها میرسند.
به عنوان مثال، نهادهای زیادی بین کلاغها وجود دارد که توانایی ساخت وسایل محاسباتی را دارند. این وسایل شبیه وسایل محاسباتی انسانها، الکترونیکی نیستند بلکه از رفتارهای حشراتی کوچک، گونههایی نزدیک به موریانه که یک رابطهی همزیستی با کلاغ ها توسعه داده اند، بهره برداری می کنند. کلاغها برایشان غذا فراهم می کنند و در عوض حشرات کلنیهای پیچیدهی آن ها را ساخته و بکار می اندازند، که به کمک موجودات زندهای که در درون آن ها زندگی می کنند، محاسبات را انجام میدهند.
این گونه کلنیها معمولا در لانههای بزرگ و قدیمی قرار دارند. پرندهها و حشرات با همکاری هم شبکهای از ساختارهای گلی پیازیشکل را میسازند و بین ترکهای لانه پنهان می کنند که در آن حشرات زندگی و کار خواهند کرد.
برای تعامل با دیگر وسایل،این ماشینها از سیگنالهای نور استفاده می کنند. کلاغها قطعاتی از مواد انعکاسی را در ساقههای خاصی که برای ارتباط در نظر گرفته شده اند جاسازی می کنند و حشرات آن ها را هدف قرار میدهند تا نور را به لانهی دیگری بتابانند و دادهها را به صورت دنبالهای از چشمکهای کوتاه به رمز در می آورند. این یعنی فقط لانههایی که دارای یک ارتباط متصل بصری هستند میتوانند با یکدیگر تعامل کنند.
دوست متخصص کلاغ ما نقشهای از شبکهی لانههای کلاغها در روستای <bdo>
Hières-sur-Amby</bdo>
قرار دارد، کشیده است که در حاشیهی رودخانهی Rhône قرار دارد. آن نقشه نشان می دهد که لانهها و ارتباطاتشان چگونه است:
در نمونهای شگفتانگیز از تکامل همگرا، کامپیوترهای کلاغها، جاوااسکریپت را اجرا می کنند. در این فصل قرار است بعضی از قابلیتهای پایهای شبکه را برایشان برنامهنویسی کنیم.
توابع callback
یکی از راههای برنامهنویسی ناهمگام این است توابعی که یک کار زمانگیر را انجام می دهند، یک آرگومان اضافی دریافت کنند، یک تابع callback. در این روش، تابع اصلی اجرا شده و پایان می پذیرد سپس تابع callback با نتایج دریافتی از تابع اصلی فراخوانی می گردد.
به عنوان یک مثال، تابع setTimeout
، که در <bdo>
Node.js</bdo>
و مرورگرها در دسترس است، به اندازهی هزارم ثانیه ای که مشخص شده است منتظر می ماند و سپس یک تابع را فراخوانی میکند.
setTimeout(() => console.log("Tick"), 500);
این انتظار معمولا خیلی کاربردهای مهمی ندارد؛ اما گاهی تواند مفید باشد مانند بهروزرسانی یک انیمیشن یا بررسی طول کشیدن چیزی در برنامه.
اجرای چندین عمل ناهمگام در یک ردیف با استفاده از توابع callback به این معنا است که شما باید به ارسال توابع جدید برای ادامهی محاسبه بعد از هر عمل ادامه دهید.
بیشتر کامپیوترهای موجود در لانههای کلاغها، دارای یک بافت ذخیرهسازی بلند مدت میباشند، جاییکه اطلاعات، درون شاخهها حک میشوند و میتوان آن ها را بعدا دوباره خواند. حک کردن یا پیدا کردن یک بخش از اطلاعات زمانگیر است بنابراین رابط سیستم ذخیرهسازی بلند مدت، ناهمگام خواهد بود و از توابع callback استفاده خواهد شد.
بافتهای ذخیرهسازی، بخشهای اطلاعات را که به فرمت JSON درآمده اند، تحت نامهایی ذخیره می کنند. یک کلاغ ممکن است اطلاعاتی در مورد مخفیگاه غذاها را به عنوان <bdo>
"food caches"
</bdo>
ذخیره کند، که میتواند دارای آرایهای از نامهایی باشد که به دیگر بخشهای اطلاعات اشاره می نمایند، اطلاعاتی که مخفیگاه واقعی را توصیف می کنند. برای جستجوی یک مخفیگاه غذا در بافتهای ذخیرهسازی لانهی <bdo>
Big Oak</bdo>
، یک کلاغ میتواند کدی مثل زیر را اجرا کند.
import {bigOak} from "./crow-tech"; bigOak.readStorage("food caches", caches => { let firstCache = caches[0]; bigOak.readStorage(firstCache, info => { console.log(info); }); });
(تمامی متغیرها و رشتهها از زبان کلاغی به زبان انگلیسی ترجمه شده اند.)
این سبک از برنامهنویسی شدنی است؛ اما با هر بار عمل ناهمگام، میزان تورفتگی اضافه میشود ،چرا که به یک تابع دیگر نیاز خواهد بود. برای انجام کارهای پیچیدهتر ، مثل انجام چند عمل در یک زمان واحد، این شیوهی کدنویسی میتواند کمی بدقواره و نامناسب شود.
کامپیوترهای لانهها، طوری ساخته شده اند که بتوانند به وسیلهی جفتهای درخواست-پاسخ با هم ارتباط برقرار کنند. این یعنی یک لانه، پیامی را به لانهی دیگری ارسال میکند، که این لانه نیز بلافاصله پیامی را که حاوی تایید دریافت و احتمالا شامل پاسخی به درخواست است برمیگرداند.
هر پیغام توسط یک نوع، برچسب گذاری میشود که تعیین کنندهی نحوهی مدیریت آن میباشد. کد ما میتواند توابعی را برای رسیدگی به انواع خاص، تعریف کند، و زمانی که درخواستی از آن نوع آمد ، تابع رسیدگیکننده فراخوانی شده تا پاسخی را تولید کند.
رابطی که توسط ماژول <bdo>
"./
</bdo>
صادر میشود، تابعی دارای callback برای تعامل فراهم میکند. لانهها دارای متدی به نام send
هستند که درخواستها را ارسال میکند. این متد نام لانهی مقصد، نوع درخواست و محتوای درخواست را به عنوان سه آرگومان اول گرفته و آرگومان بعدی یک تابع است که زمانی که یک پاسخ دریافت می شود، فراخوانی میشود.
bigOak.send("Cow Pasture", "note", "Let's caw loudly at 7PM", () => console.log("Note delivered."));
اما برای اینکه لانهها را قادر سازیم تا آن درخواست را دریافت کند، میبایست ابتدا نوع درخواستی به نام "note"
را تعریف کنیم. کدی که به این درخواستها رسیدگی میکند باید نه تنها بر روی کامپیوتر این لانه اجرا شود بلکه باید روی تمامی لانههایی که میتوانند پیامی از این نوع را دریافت کنند اجرا شود. ما فرض می کنیم که کلاغی پرواز کرده و کد ما را روی همهی لانهها نصب میکند.
import {defineRequestType} from "./crow-tech"; defineRequestType("note", (nest, content, source, done) => { console.log(`${nest.name} received note: ${content}`); done(); });
تابع defineRequestType
یک نوع درخواست جدید را تعریف میکند. در مثال، امکان پشتیبانی از درخواستهای "note"
اضافه میشود که در واقع تنها یک یادداشت را به لانهی داده شده ارسال میکند. پیادهسازی ما، از <bdo>
console.log
</bdo>
برای تایید رسیدن درخواست استفاده میکند. لانهها دارای خاصیتی به نام name
هستند که نامشان را نگه داری میکند.
آرگومان چهارمی که به تابع رسیدگی کننده داده میشود، done
، یک تابع callback است که باید زمانی که درخواست کارش تمام شد فراخوانی شود. اگر از مقدار بازگشتی (توسط return) از تابع رسیدگی کننده به عنوان مقدار پاسخ استفاده کرده بودیم ، در اینصورت رسیدگیکنندهی درخواست نمیتوانست خودش یک عمل ناهمگام را اجرا کند. تابعی که یک کار ناهمگام را انجام می دهد نوعا قبل از انجام آن کار برمیگردد و برای اجرای تابع callback پس از انجام کار تنظیم میشود. بنابراین ما نیاز به مکانیزمهایی ناهمگام داریم – در این مثال، به یک تابع callback دیگر- تا وقتی که یک پاسخ آماده بود، علامت بدهیم.
به شکلی، ناهمگامی مسری است. هر تابعی که یک تابع را فراخوانی کند که به صورت ناهمگام عمل میکند، خودش باید ناهمگام باشد که میتوان با استفاده از یک callback یا مکانیزمی شبیه به آن باشد تا نتیجهاش را تحویل دهد. فراخوانی یک callback، از برگرداندن یک مقدار به شکل ساده، کمی پیچیده تر و مشکلساز تر است؛ بنابراین استفاده از این روش برای ساختاردهی بخشهای بزرگی از برنامهتان جالب نیست.
Promise ها
اگر بتوان مفاهیم مجرد را به صورت مقدارها نمایش داد، اغلب درکشان ساده تر میشود. در رابطه با کارهای ناهمگام میتوانید به جای تنظیم یک تابع برای فراخوانی در یک نقطهی خاص در آینده، یک شیء را برگردانید که این رخداد آینده را نمایندگی کند.
این دقیقا چیزی است که کلاس استاندارد Promise
انجام می دهد. یک promise یک عمل ناهمگام است که در زمانی تکمیل میشود و مقداری را تولید میکند. میتواند هرکسی که علاقمند باشد را در زمان آماده شدن مقدارش باخبر کند.
آسان ترین روش ایجاد یک promise فراخوانی <bdo>
Promise.resolve
</bdo>
است. این تابع اطمینان حاصل میکند که مقداری که به آن می دهید درون یک promise قرار میگیرد. اگر خودش از قبل یک promise بود، برگردانده می شود – در غیر این صورت، شما promise جدیدی دریافت میکنید که با مقدار شما به عنوان نتیجهاش بلافاصله پایان میپذیرد.
let fifteen = Promise.resolve(15); fifteen.then(value => console.log(`Got ${value}`)); // → Got 15
برای گرفتن نتیجهی یک promise، میتوانید از متد then
آن استفاده کنید. این متد تابع callbackای را ثبت میکند که در هنگامی که promise به نتیجه رسید و مقداری را تولید کرد، فراخوانی میشود. میتوانید چندین تابع callback را به یک promise اضافه کنید، و همهی آنها فراخوانی خواهند شد، حتی اگر آنها را بعد از به نتیجهرسیدن promise اضافه کنید.
اما این همهی آن چیزی نیست که متد then
انجام می دهد. این متد promise دیگری را برمیگرداند، که مقداری که از تابع رسیدگی کننده برمی گردد را (resolve) را نتیجهیابی میکند یا اگر یک promise را برگرداند، برای آن promise منتظر می ماند سپس به حل و فصل نتیجهاش می پردازد.
خوب است که promiseها را به عنوان وسایلی که مقدارها را به درون فضای ناهمگام انتقال میدهند تصور کنید. یک مقدار نرمال به صورت طبیعی وجود دارد. یک مقدار وعده داده شده (promised value) مقداری است که ممکن است از قبل وجود داشته باشد یا در نقطهای در آینده ظاهر شود. محاسباتی که به عنوان promise تعریف میشوند روی این گونه مقدارها عمل می کنند و همزمان با در دسترس قرار گرفتن مقدارها به اجرا در می آیند.
برای ایجاد یک promise، میتوانید از Promise
به عنوان یک سازنده استفاده کنید. رابط آن کمی غیرمعمول است – سازنده، یک تابع را به عنوان آرگومان میگیرد که آن را بلافاصله فراخوانی میکند و تابعی به آن ارسال میکند که این تابع میتواند برای نتیجه یابی promise استفاده شود. این سازنده به صورتی که گفته شد کار میکند نه مثلا به شکلی که با یک متد resolve
کار کند و فقط کدی که promise را ایجاد کرده بتواند آن را نتیجهیابی کند.
این روشی است که میتوانید برای ایجاد یک رابط مبتنی بر promise برای تابع readStorage
ایجاد کنید:
function storage(nest, name) { return new Promise(resolve => { nest.readStorage(name, result => resolve(result)); }); } storage(bigOak, "enemies") .then(value => console.log("Got", value));
این تابع ناهمگام یک مقدار معنادار را تولید میکند. این مزیت اصلی promise ها است – آن ها استفاده از توابع ناهمگام را ساده می کنند. به جای اینکه مجبور باشیم callbackهای متعددی ارسال کنیم، توابع مبتنی بر promise شبیه توابع معمولی به نظر می رسند: ورودی ها را به عنوان آرگومان می گیرند و خروجی شان را تولید می کنند. تنها تفاوت این است که خروجی ممکن است هنوز در دسترس نباشد.
شکست
محاسبات عادی جاوااسکریپت میتوانند با شکست روبرو شده و یک استثنا را تولید کنند. محاسبات ناهمگام هم اغلب به چیزی شبیه به آن نیاز دارند. ممکن است یک درخواست در شبکه با شکست روبرو شود یا کدی که بخشی از یک محاسبهی ناهمگام است استثنایی را تولید کند.
یکی از حیاتیترین مشکلاتی که در سبک مبتنی بر callback برنامهنویسی ناهمگام وجود دارد این است که در این سبک، گزارش صحیح شکستها به توابع callback بسیار دشوار است.
یکی از راه حل های رایج برای آن این است که آرگومان اول callback را برای مشخص کردن شکست عمل در نظر می گیرند و دومین آرگومان، حاوی مقداری خواهد بود که در صورت موفقیت عمل، تولید میشود. این گونه توابع callback باید همیشه بررسی کنند که آیا استثنایی دریافت کرده اند یا خیر و اطمینان حاصل کنند که هر مشکلی که ایجاد می کنند، مانند استثناهای تولیدی توسط توابعی که فراخوانی میکنند، مدیریت شده و به تابع درستی داده میشود.
promiseها این کار را ساده تر کرده اند. میتوان آن ها را resolve (نتیجه یابی ) کرد (عمل با موفقیت به پایان رسیده) یا رد (reject) کرد (شکست خورده است). توابع رسیدگی کننده به موفقیت (که با متد then
ثبت شده اند) فقط زمانی فراخوانی میشوند که عمل باموفقیت انجام شده باشد و rejectها به صورت خودکار به یک promise جدید سپرده میشوند که توسط then
برگردانده میشود. و زمانی که یک تابع گرداننده (handler) استثنا تولید میکند، این به طور خودکار سبب میشود که promise ای که توسط فراخوانی متد thenاش تولید شده است رد بشود. بنابراین اگر یکی از عناصری که در زنجیرهی اعمال ناهمگام قرار دارد با شکست روبرو شود، خروجی تمام زنجیره به عنوان “رد شده” یا rejected در نظر گرفته میشود، و هیچ تابع گردانندهی دیگری بعد از نقطهای که با مشکل روبرو شده است فراخوانی نمیشود.
بسیار شبیه به نتیجهیابی یک promise که مقداری را فراهم می ساخت، رد شدن آن نیز مقداری را فراهم میکند، که معمولا به عنوان دلیل رد شدن شناخته میشود. زمانی که یک استثنا در یک تابع گرداننده باعث رد شدن میشود، مقدار استثنا به عنوان دلیل استفاده میشود. به طور مشابه زمانی که یک گرداننده، یک promise را برمی گرداند که رد شده است، این پذیرفتهنشدن به درون promise بعدی جریان می یابد. تابعی به نام <bdo>
Promise.reject
</bdo>
وجود دارد که یک promise رد شده جدید بلافاصله ایجاد میکند.
رای رسیدگی صریح به این گونه رد شدنها، promise ها دارای متدی به نام catch
هستند که یک گرداننده را برای فراخوانی در هنگام رد شدن ثبت میکند، شبیه به گردانندههای then
که در موارد یافتن نتیجه صحیح استفاده می شدند. از این لحاظ نیز بسیار شبیه به then
است که یک promise جدید برمی گرداند که در صورت نتیجهیابی بدون مشکل به نتیجهی promise اصلی منجر میشود و در غیر این صورت به نتیجهی گردانندهی catch
. اگر یک گردانندهی catch
خطایی تولید کند، promise جدید نیز رد میشود.
به عنوان یک راه خلاصه تر، متد then
همچنین یک گردانندهی عدم پذیرش نیز به عنوان آرگومان دوم قبول میکند، بنابراین میتوانید هر دوی گردانندهها را با یک فراخوانی متد ثبت کنید.
تابعی که به سازندهی Promise
ارسال میشود در کنار تابع موفقیت (resolve) آرگومان دومی را دریافت میکند، که میتواند برای رد کردن promise جدید استفاده شود.
زنجیرهی مقدارهای promise که با فراخوانیهایی که به then
و catch
زده شده است تولید شده را میتوان به عنوان یک خط لوله در طول مقدارهای ناهمگام یا حرکتهای منجر به شکست دانست. به دلیل این که این زنجیره به وسیلهی ثبت گردانندهها تولید میشود، هر پیوند دارای یک گردانندهی موفقیت یا عدم پذیرش (یا هر دو) است که به آن ارتباط دارد. گردانندههایی که تطبیقی با نوع خروجی (موفقیت یا شکست) ندارند در نظر گرفته نمیشوند. اما آن هایی که هماهنگ هستند فراخوانی میشوند و خروجی آن ها مشخص می کند چه نوع مقداری در ادامه خواهد آمد – موفقیت در زمانی که یک مقدار غیر promise بر می گرداند، عدم پذیرش زمانی که یک استثنا تولید میشود، و خروجی یک promise زمانی که یکی از آن ها را بر می گرداند.
new Promise((_, reject) => reject(new Error("Fail"))) .then(value => console.log("Handler 1")) .catch(reason => { console.log("Caught failure " + reason); return "nothing"; }) .then(value => console.log("Handler 2", value)); // → Caught failure Error: Fail // → Handler 2 nothing
بسیار شبیه به یک استثنای مدیریت نشده که توسط محیط رسیدگی میشود ، محیط های جاوااسکریپت میتوانند تشخیص دهند در چه زمانی یک عدم موفقیت promise رسیدگی نشده است و آن را به عنوان یک خطا گزارش خواهند داد.
شبکهها دشوار هستند
گاهی اوقات، نور کافی برای سیستم انعکاس نور کلاغها بهمنظور انتقال سیگنال وجود ندارد، یا چیزی مسیر سیگنال را مسدود کرده است. ممکن است سیگنالی فرستاده شود ولی هرگز دریافت نشود.
در این صورت، این باعث میشود که تابع callback ای که به متد send
داده شده است هرگز فراخوانی نشود، که احتمالا موجب توقف برنامه بدون هیچ گونه اعلام مشکل میشود. خوب بود اگر بعد از یک دورهی زمانی مشخص که پاسخی دریافت نشد، یک درخواست به صورت خودکار منقضی می شد و یک شکست گزارش می شد.
اغلب، شکست های مربوط به ارسال به صورت تصادفی اتفاق می افتند، مانند بروز تداخل بین چراغ جلوی یک خودرو با سیگنالهای نوری، و در این صورت فقط دوباره فرستان درخواست مشکل را برطرف میکند. بنابراین هنگامی که هنوز در آن نقطه قرار داریم، اجازه بدهید تابع درخواست را طوری تنظیم کنیم که به طور خودکار قبل از اینکه دست از کار بکشد چندین بار درخواست را ارسال کند.
و به دلیل اینکه قبول کرده ایم که promise ها مفید هستند، پس تابع درخواست را تغییر می دهیم تا یک promise برگرداند. در رابطه با کاری که میتوانند انجام دهند تفاوتی بین callback ها و promise ها وجود ندارد. توابع مبتنی بر callback را میتوان پوشاند به شکلی که رابطی promise گونه داشته باشند و همین طور برعکس.
حتی زمانی که یک درخواست و پاسخ آن با موفقیت تحویل داده میشوند، پاسخ ممکن است نشانگر یک شکست باشد – به عنوان مثال، اگر درخواست تلاش کند که از نوع درخواستی استفاده کند که تعریف نشده است یا گرداننده یک خطا تولید کند. برای پشتیبانی از این، send
و defineRequestType
از قراردادی پیروی می کنند که قبل تر ذکر شد جاییکه اولین آرگومان فرستاده شده با تابع callback، دلیل شکست خواهد بود، در صورت وجود البته، و دومین آرگومان نتیجهی واقعی خواهد بود.
اینها را میتوان به وسیلهی یک پوشاننده (wrapper) به پذیرش و عدم پذیرش promise ترجمه کرد.
class Timeout extends Error {} function request(nest, target, type, content) { return new Promise((resolve, reject) => { let done = false; function attempt(n) { nest.send(target, type, content, (failed, value) => { done = true; if (failed) reject(failed); else resolve(value); }); setTimeout(() => { if (done) return; else if (n < 3) attempt(n + 1); else reject(new Timeout("Timed out")); }, 250); } attempt(1); }); }
به دلیل اینکه promise ها میتوانند فقط یک بار موفق شوند (یا رد بشوند)، این روش کار خواهد کرد. اولین باری که resolve
یا reject
فراخوانی میشوند، خروجی promise را معین می کنند، و هر فراخوانی ای در بعد، مانند timeout که بعد از پایان درخواست می رسد یا درخواستی که بعد از یک پایان یک درخواست دیگر برمی گردد، در نظر گرفته نمی شوند.
برای ساخت یک حلقهی ناهمگام، برای تلاشهای اضافی، لازم است تا از یک تابع بازگشتی استفاده کنیم – یک حلقهی معمولی امکان توقف و صبر برای یک عمل ناهمگام را فراهم نمی کند. تابع attemp
یک تلاش واحد برای ارسال یک درخواست ترتیب می دهد. همچنین یک زمان انقضا تنظیم میکند، اگر پاسخی بعد از 250 هزارم ثانیه نیامد ، یا تلاش بعد را شروع کند یا اگر این چهارمین تلاش بود ، promise را رد میکند و به عنوان دلیل عدم پذیرش هم یک نمونه از Timeout
را استفاده میکند.
تلاش مجدد هر یک چهارم ثانیه و توقف در صورت نیامدن پاسخ پس از گذشت یک ثانیه، قطعاً دلخواه است. البته حتی ممکن است که درخواست دریافت شود اما تابع گرداننده کند عمل کند که باعث شود عمل دریافت چندین بار صورت گیرد. ما توابع گرداننده را طوری می نویسیم که این مشکل را پوشش دهیم و پیغامهای تکراری ضرری برای سیستم نداشته باشند.
طبیعتا قرار نیست که یک شبکهی بی نقص در سطح جهانی را امروز بسازیم. اما قابل قبول خواهد بود- کلاغها انتظارات خیلی بالایی در رابطه با محاسبات ندارند.
برای اینکه خودمان را به طور کامل از callback ها رها کنیم، پیشتر خواهیم رفت و همچنین یک پوشش برای تابع defineRequestType
تعریف خواهیم کرد که به تابع گرداننده اجازه بدهد تا یک promise یا مقداری ساده را برگرداند و آن را به callback برای ما متصل کند.
function requestType(name, handler) { defineRequestType(name, (nest, content, source, callback) => { try { Promise.resolve(handler(nest, content, source)) .then(response => callback(null, response), failure => callback(failure)); } catch (exception) { callback(exception); } }); }
<bdo>
Promise.resolve
</bdo>
برای تبدیل مقدار بازگشتی از handler
به یک promise استفاده میشود؛ اگر قبلا انجام نشده باشد.
توجه داشته باشید فراخوانی به تابع handler
باید درون یک بلاک try
قرار می گرفت، تا اطمینان حاصل شود هر استثنایی که تولید میکند مستقیما به تابع callback داده می شود. این به خوبی سختی رسیدگی درست به خطاها در مدل callback های خام را نشان می دهد – به راحتی ممکن است مدیریت صحیح استثناها را فراموش کنیم؛ مانند بالا و اگر این کار را انجام ندهید، شکستها به callback درستی گزارش نمیشوند. در promise ها، این کار را به طور خودکار انجام میشود بنابراین کمتر خطاساز خواهند بود.
مجموعهای از promise ها
هر کامپیوتر لانه دارای آرایهای از دیگر لانهها میباشد که درون محدودهی مخابره قرار دارند و آن را در خاصیت neighbors
آن ذخیره میکند. برای بررسی اینکه کدام یک از آن ها در حال حاضر در دسترس هستند، میتوانید تابعی بنویسید که تلاش کند تا یک درخواست “ping”
(درخواستی که فقط برای دریافت پاسخ ارسال میشود) را به هر یک از لانه ها ارسال کند و مشاهده کنید کدام درخواست پاسخ داده میشود.
زمانی که با مجموعهای از promise ها کار میکنید که در یک زمان یکسان اجرا میشوند، تابع <bdo>
Promise.all
</bdo>
میتواند مفید باشد. این تابع یک promise را برمی گرداند که برای همهی promise های درون آرایه صبر میکند تا به نتیجه برسند و بعد نتیجه را درون یک آرایه از مقدارهایی که این promise ها تولید کرده اند می ریزد (به همان ترتیبی که در آرایهی اصلی آمده بودند). اگر یک promise رد شده باشد، نتیجهی <bdo>
Promise.all
</bdo>
خودش نیز رد میشود.
requestType("ping", () => "pong"); function availableNeighbors(nest) { let requests = nest.neighbors.map(neighbor => { return request(nest, neighbor, "ping") .then(() => true, () => false); }); return Promise.all(requests).then(result => { return nest.neighbors.filter((_, i) => result[i]); }); }
زمانی که یک همسایه در دسترس نیست، دوست نداریم که تمامی promise ترکیب شده با شکست روبرو شود، در آن موقع ما هنوز چیزی نمی دانیم. بنابراین تابعی که بر روی مجموعهی همسایهها اعمال شده است تا هر یک از آن ها را به درخواستهای promise تبدیل کند، گردانندههایی الصاق میکند تا درخواست های موفق true
را تولید کنند و رد شده ها false
را برگردانند.
در گردانندهای که برای promise ترکیبی در نظر گرفته شده، filter
کار حذف آن عناصر، عناصری که مقدار متناظرشان برابر با false باشد را از آرایهی neighbors
انجام میدهد. این کار از این واقعیت بهره می برد که filter
اندیس عنصر فعلی در آرایه را به عنوان آرگومان دومش به تابع فیلترش (مانند map
، some
یا دیگر توابع ردهبالای آرایهها که به صورت مشابه عمل می کنند) ارسال میکند.
جریان سیلگونه در شبکه
این واقعیت که لانهها فقط میتوانند با همسایههایشان ارتباط برقرار کنند در مفید بودن این شبکه مانع ایجاد میکند.
برای رساندن اطلاعات به کل شبکه، یک راه حل این است که نوع درخواستی تنظیم شود که به صورت خودکار به دیگر همسایهها مخابره شود. این همسایه ها سپس آن اطلاعات را به همسایههایشان منتقل می کنند تا زمانی که کل شبکه پیام را گرفته باشد.
import {everywhere} from "./crow-tech"; everywhere(nest => { nest.state.gossip = []; }); function sendGossip(nest, message, exceptFor = null) { nest.state.gossip.push(message); for (let neighbor of nest.neighbors) { if (neighbor == exceptFor) continue; request(nest, neighbor, "gossip", message); } } requestType("gossip", (nest, message, source) => { if (nest.state.gossip.includes(message)) return; console.log(`${nest.name} received gossip '${ message}' from ${source}`); sendGossip(nest, message, source); });
برای جلوگیری از ارسال مداوم یک پیام یکسان در شبکه، هر لانه آرایهای از رشتههایی که قبلا دیده شده اند را نگه داری میکند. برای تعریف این آرایه، از تابع everywhere
استفاده می کنیم – که کد را روی هر لانه اجرا میکند – برای افزودن یک خاصیت به شیء state لانه، که جایی است که ما وضعیت محلی لانه را نگه داری خواهیم کرد.
زمانی که یک لانه یک پیام تکراری را دریافت کند، که احتمالش در جایی که هر لانه پیامها را ندید بازارسال میکند وجود دارد، از آن پیام صرف نظر میشود. اما زمانی که پیامی جدید را دریافت میکند، آن پیام را با هیجان به همهی لانهها به جز لانهی فرستندهی پیام، ارسال میکند.
این کار درست مانند پخش شدن جوهر در آب، خبر جدید را در شبکه پخش میکند. حتی زمانی که بعضی از ارتباطات در دسترس نیستند، اگر مسیر جایگزینی به یک لانهی مشخص وجود داشته باشد، خبر از آن طریق به آن لانه خواهد رسید.
این سبک از ارتباطات شبکهای را سیلگونه (flooding) می گویند – مانند سیل تمام شبکه را با اطلاعات فرا میگیرد تا این که همهی گره ها را پوشش دهد.
با فراخوانی sendGossip
میتوانیم جریان پیغام درون روستا را مشاهده کنیم.
sendGossip(bigOak, "Kids with airgun in the park");
مسیردهی پیام
اگر یک گره بخواهید با یک گرهی مشخص دیگر ارتباط برقرار کند، سبک سیلگونه زیاد بهینه عمل نخواهد کرد. مخصوصا زمانی که شبکه بزرگ باشد، که باعث میشود میزان زیادی مخابره اطلاعات بدون کاربرد صورت گیرد.
یک راه حل جایگزین این است که برای پیامها راهی در نظر گرفته شود تا از گرهای به گرههای دیگر بپرند تا به گرهی مقصد برسند. مشکل این روش این است که باید اطلاعاتی دربارهی نقشهی شبکه داشته باشیم. برای ارسال درخواستی به سمت یک گره دور، لازم است بدانیم کدام لانههای همسایه پیام را به مقصد نزدیک تر می کنند. ارسال آن به سمتی اشتباه ما را به هدف نمیرساند.
به دلیل اینکه هر لانه فقط همسایههای مجاورش را میشناسد، اطلاعات کافی برای محاسبه یک مسیر را در دست ندارد. باید به شیوهای اطلاعات این اتصالات را بین همهی لانهها منتشر کنیم. ترجیحا به روشی که بتوان در آینده در آن تغییر ایجاد کرد مثلا زمانیکه یک لانه متروکه میشود یا لانهی جدیدی ساخته میشود.
میتوانیم دوباره به سراغ روش سیلگونه برویم، اما به جای استفاده از آن برای بررسی دریافت یک پیام، اکنون بررسی می کنیم آیا مجموعهی همسایهها برای یک گرهی مشخص با مجموعهای که اکنون برای آن در دسترس داریم مطابقت دارد یا خیر.
requestType("connections", (nest, {name, neighbors}, source) => { let connections = nest.state.connections; if (JSON.stringify(connections.get(name)) == JSON.stringify(neighbors)) return; connections.set(name, neighbors); broadcastConnections(nest, name, source); }); function broadcastConnections(nest, name, exceptFor = null) { for (let neighbor of nest.neighbors) { if (neighbor == exceptFor) continue; request(nest, neighbor, "connections", { name, neighbors: nest.state.connections.get(name) }); } } everywhere(nest => { nest.state.connections = new Map; nest.state.connections.set(nest.name, nest.neighbors); broadcastConnections(nest, nest.name); });
در مقایسه از <bdo>
JSON.stringify
</bdo>
استفاده میشود چرا که ==
، روی اشیاء و آرایهها، فقط زمانی true برمی گرداند که هر دو طرف دارای مقدار یکسانی باشند، که چیزی نیست که ما در اینجا لازم داریم. مقایسهی رشتههای JSON جالب به نظر نمیرسد اما روشی موثر برای مقایسهی محتوای آن ها است.
گرهها بلافاصله شروع به مخابرهی اتصالاتشان می کنند، که باید به سرعت به هر لانه یک نقشه از گراف فعلی شبکه را بدهد، مگر اینکه بعضی از لانهها کلا در دسترس نباشند.
یکی از کارهایی که در گرافها میتوان انجام داد پیدا کردن مسیرها در آنها است ، همانطور که در فصل 7 دیدیم. اگر مسیری به سمت یک مقصد پیام داشته باشیم، می دانیم از کدام جهت باید اقدام به ارسال آن کنیم.
این تابع findRoute
، که بسیار شباهت به تابع findRoute
فصل 7 دارد، برای رسیدن به یک گره مشخص شده در شبکه به جستجو می پردازد. اما به جای برگرداندن تمام مسیر، فقط گام بعدی را برمی گرداند. لانهی بعدی خودش، از اطلاعات فعلی اش در رابطه با شبکه استفاده خواهد کرد و تصمیم میگیرد که کجا پیغام را بفرستد.
function findRoute(from, to, connections) { let work = [{at: from, via: null}]; for (let i = 0; i < work.length; i++) { let {at, via} = work[i]; for (let next of connections.get(at) || []) { if (next == to) return via; if (!work.some(w => w.at == next)) { work.push({at: next, via: via || next}); } } } return null; }
اکنون میتوانیم تابعی بسازیم که میتواند پیغامها را به نقاط دور ارسال کند. اگر پیام مورد نظر مقصدش یک همسایهی مجاور بود، به طور معمولی تحویل داده میشود. در غیر این صورت، درون یک شیء قرار گرفته و به همسایهای ارسال میشود که به هدف نزدیک تر است، با استفاده از نوع درخواست "route"
که باعث میشود آن همسایه نیز این رفتار را تکرار کند.
function routeRequest(nest, target, type, content) { if (nest.neighbors.includes(target)) { return request(nest, target, type, content); } else { let via = findRoute(nest.name, target, nest.state.connections); if (!via) throw new Error(`No route to ${target}`); return request(nest, via, "route", {target, type, content}); } } requestType("route", (nest, {target, type, content}) => { return routeRequest(nest, target, type, content); });
اکنون میتوانیم پیامی به لانهای که در برج کلیسا قرار دارد ارسال کنیم که چهار گام در شبکه نیاز دارد.
routeRequest(bigOak, "Church Tower", "note", "Incoming jackdaws!");
تاکنون لایههای متعددی از قابلیتها را روی یک سیستم ارتباطی اولیه ساخته ایم تا استفاده از آن را راحت و سرراست کنیم. این مدل (البته ساده شدهی) خوبی از چگونگی عملکرد شبکههای کامپیوتر در واقعیت است.
یک خاصیت متمایز کننده در شبکههای کامپیوتری این است که آن ها قابل اتکا نیستند – تجریدهایی که بر اساس آنها انجام می شود میتوانند مفید باشند، اما شکست شبکه را نمیتوان با آنها پوشش داد. بنابراین برنامهنویسی تحت شبکه نوعا با انتظار خرابی (failure) در شبکه و مدیریت آن سر و کار دارد.
توابع Async
برای ذخیرهی اطلاعات مهم، کلاغها اطلاعات را بین لانهها تکثیر می کنند. در این روش ، زمانی که یک شاهین یکی از لانهها را از بین می برد، اطلاعات از بین نخواهند رفت.
برای بازیابی یک بخش از اطلاعات که در بافت موجود در خود لانه وجود ندارد، یک کامپیوتر لانه ممکن است با لانههای تصادفی در شبکه ارتباط بگیرد تا اینکه آن لانهای که اطلاعات را دارد پیدا شود.
requestType("storage", (nest, name) => storage(nest, name)); function findInStorage(nest, name) { return storage(nest, name).then(found => { if (found != null) return found; else return findInRemoteStorage(nest, name); }); } function network(nest) { return Array.from(nest.state.connections.keys()); } function findInRemoteStorage(nest, name) { let sources = network(nest).filter(n => n != nest.name); function next() { if (sources.length == 0) { return Promise.reject(new Error("Not found")); } else { let source = sources[Math.floor(Math.random() * sources.length)]; sources = sources.filter(n => n != source); return routeRequest(nest, source, "storage", name) .then(value => value != null ? value : next(), next); } } return next(); }
به دلیل اینکه connections
از جنس Map
است، <bdo>
Object.keys
</bdo>
روی آن جواب نمی دهد. متد keys در این شیء هم وجود دارد اما یک تکرارکننده (iterator) را برمی گرداند نه یک آرایه. یک تکرارکننده (یا مقدار قابل تکرار) را میتوان به وسیلهی <bdo>
Array.from
</bdo>
به آرایه تبدیل کرد.
حتی با وجود استفاده از promise ها این کد نسبتا شکل خوبی ندارد. عملیات متعدد ناهمگام با هم زنجیر شده اند به صورتی که اصلا خوانا و واضح نیست. دوباره نیاز به یک تابع بازگشتی داریم (next
) تا بتوانیم حلقه (looping) را بین لانهها مدل سازی کنیم.
و این که کاری که این کد درواقع انجام می دهد کاملا خطی است – همیشه منتظر اتمام عمل قبلی پیش از شروع عمل بعدی می ماند. در یک مدل برنامهنویسی همگام ، سادهتر می توان این کارها را پیاده سازی کرد.
خبر خوب این است که جاوااسکریپت این امکان را فراهم کرده است که کدهای شبه-همگام بنویسید. یک تابع async
تابعی است که به طور ضمنی یک promise را برمیگرداند و میتواند در بدنهاش ، به وسیلهی دستور await
منتظر دیگر promiseها باشد به طوری که همگام به نظر برسد.
میتوانیم تابع findInStorage
را به شکل زیر بازنویسی کنیم.
async function findInStorage(nest, name) { let local = await storage(nest, name); if (local != null) return local; let sources = network(nest).filter(n => n != nest.name); while (sources.length > 0) { let source = sources[Math.floor(Math.random() * sources.length)]; sources = sources.filter(n => n != source); try { let found = await routeRequest(nest, source, "storage", name); if (found != null) return found; } catch (_) {} } throw new Error("Not found"); }
یک تابع async را میتوان با واژهی async
قبل از کلیدواژهی function
مشخص کرد. متدها را نیز میتوان با نوشتن آن قبل از نام متد تبدیل به async
کرد . زمانی که تابع یا متدی با این خصوصیت فراخوانی شود یک promise را تولید خواهد کرد. به محض این که بدنهی تابع چیزی را برگرداند، آن promise نتیجهیابی میشود. اگر استثنایی تولید کند، promise رد میشود.
findInStorage(bigOak, "events on 2017-12-21") .then(console.log);
درون یک تابع async،
واژهی await
را میتوان در ابتدای یک عبارت قرار داد تا تابع برای دریافت نتیجهی promise منتظر بماند و بعد از آن به ادامهی اجرای تابع بپردازد.
این گونه توابع دیگر مانند توابع معمولی جاوااسکریپت از ابتدا تا انتها در یک حرکت اجرا نمیشوند. بلکه ممکن است در هر نقطهای که یک await
دارند ایست کنند و بعدا به ادامه مسیرشان بپردازند.
برای کدهای ناهمگام مهم، استفاده از این روش معمولا مناسب تر است از استفاده از promise ها. حتی اگر لازم است که کاری انجام بدهید که مناسب مدل همگام نیست، مثل اجرای چندین کار در یک زمان، به آسانی میتوان await
را با استفاده مستقیم از promise ها ترکیب کرد.
مولدها Generators
این قابلیت در توابع که میتوانند متوقف شده و بعدا دوباره به مسیرشان ادامه بدهند فقط مخصوص به توابع async
نیست. جاوااسکریپت قابلیتی به نام توابع generator (مولد) دارد. این توابع به طور مشابه عمل می کنند اما بدون promise ها.
زمانی که تابعی را با <bdo>
function*
</bdo>
(یک ستاره بعد از کلیدواژهی function قرار می دهید)، تعریف میکنید، باعث میشود که آن تابع به یک مولد تبدیل شود. زمانی که یک تابع مولد فراخوانی میشود، یک تکرارکننده (iterator) را برمی گرداند که پیش تر در فصل 6 دیده ایم.
function* powers(n) { for (let current = n;; current *= n) { yield current; } } for (let power of powers(3)) { if (power > 50) break; console.log(power); } // → 3 // → 9 // → 27
در ابتدا، وقتی که تابع powers
را فراخوانی میکنید، تابع در ابتدای خودش ایست می کند. هر بار که next
را روی تکرارکننده فراخوانی میکنید، تابع تا رسیدن به یک عبارت yield
اجرا میشود، و دوباره متوقف شده و مقداری که به وسیلهی yield
حاصل شده است به عنوان مقدار بعدی تولیدی توسط تکرارکننده در نظر گرفته میشود. زمانی که تابع به پایان میرسد (که در این مثال هرگز اتفاق نمی افتد) تکرارکننده نیز به پایان میرسد.
نوشتن تکرارکنندهها اغلب در هنگام استفاده از توابع مولد ساده تر میباشد. تکرارکنندهی مربوط به کلاس Groupe
(مربوط به تمرین فصل 6) را میتوان با این مولد بازنویسی کرد:
Group.prototype[Symbol.iterator] = function*() { for (let i = 0; i < this.members.length; i++) { yield this.members[i]; } };
دیگر نیازی نیست که یک شیء را ایجاد کرده تا وضعیت تکرار را نگه داری کنیم – مولدها این کار را به صورت خودکار با ذخیرهی وضعیت محلیشان با هر بار خواندن yield انجام میدهند.
عبارتهای yield
فقط میتوانند مستقیما درون خود تابع مولد استفاده شوند نه درون تابعی که درون مولد تعریف میکنید. وضعیتی که یک مولد در هنگام اجرای yield ذخیره میکند ، فقط شامل محیط محلی آن و موقعیتی که در آنجا yield انجام شده میشود.
یک تابع async
یک نوع خاص از یک مولد است. در هنگام فراخوانی یک promise تولید می کند که در هنگام پایان تابع به نتیجه میرسد و زمانی که یک استثنا تولید می کنند reject میشوند. هر وقت که این تابع یک promise را yield میکند (به عبارتی با await
منتظر یک promise می ماند)، نتیجهی آن promise (مقدار یا استثنای تولید شده) نتیجهی عبارت await
خواهد بود.
حلقهی رخداد - event loop
برنامههای ناهمگام به صورت بخش بخش اجرا میشوند. هر بخش ممکن است کارهایی را شروع کند و کدهایی را هم برنامه ریزی کند که در صورت پایان یا شکست آن کارها اجرا شوند. بین این بخش ها، برنامه بیکار می نشیند و منتظر کار بعدی خواهد ماند.
بنابراین callbackها به طور مستقیم توسط کدی که آن ها را زمانبندی کرده اند فراخوانی نمیشوند. اگر من تابع setTimeout
را از درون یک تابع فراخوانی کنم، آن تابع زمانی برگردانده میشود که تابع callback فراخوانی می شود. و زمانی که تابع callback اجرا و برمیگردد، کنترل برنامه به تابعی که آن را زمانبندی کرده بود بر نخواهد گشت.
رفتار ناهمگام، در پشتهی فراخوانی تهی تابع خودش اتفاق میافتد. این یکی از دلایلی است که بدون استفاده از promiseها، مدیریت استثناها در کدهای ناهمگام مشکل است. به دلیل اینکه هر callback با یک پشتهی تقریبا خالی شروع میشود، گردانندههای catch
شما در پشته در هنگام بروز یک استثنا در پشته نخواهند بود.
try { setTimeout(() => { throw new Error("Woosh"); }, 20); } catch (_) { // This will not run console.log("Caught!"); }
اهمیتی ندارد چقدر این رخدادها به هم نزدیک باشند- مانند timeoutها یا درخواستهای وارده – ، یک محیط جاوااسکریپت فقط یک برنامه را در یک لحظه اجرا میکند. میتوان این را به عنوان اجرای یک حلقهی بزرگ دور برنامه شما تصور کرد که به آن حلقهی رخداد (event loop) می گویند. وقتی کاری دیگر برای انجام نمانده باشد ، حلقه از کار می ایستد. اما با ورود رخدادها، آنها به یک صف اضافه میشوند و کدهایشان یکی بعد از دیگری اجرا میشوند. بدلیل اینکه هیچگاه دو کار در یک لحظه اجرا نمیشود، کدهای کند و زمانگیر ممکن است در رسیدگی به دیگر رخدادها تاخیر ایجاد کنند.
در این مثال یک timeout تنظیم میشود، اما اجرای آن به بعد از زمان اجرای در نظر گرفته شده به تاخیر میافتد.
let start = Date.now(); setTimeout(() => { console.log("Timeout ran at", Date.now() - start); }, 20); while (Date.now() < start + 50) {} console.log("Wasted time until", Date.now() - start); // → Wasted time until 50 // → Timeout ran at 55
promise ها همیشه به عنوان یک رخداد جدید، رد یا حل و فصل میشوند. حتی اگر یک promise از پیش به نتیجه رسیده باشد، انتظار برای آن باعث میشود که callback شما بعد از پایان اسکریپت کنونی اجرا شود، نه به صورت فوری.
Promise.resolve("Done").then(console.log); console.log("Me first!"); // → Me first! // → Done
در فصلهای بعدی انواع مختلفی از رخدادها را مشاهده خواهیم کرد که روی حلقهی رخدادها اجرا میشوند.
باگها در مدل برنامهنویسی ناهمگام
زمانی که برنامهی شما به صورت همگام اجرا میشود، در یک اجرای واحد، هیچ تغییر وضعیتی به جز آن هایی که خود برنامه ایجاد میکند وجود ندارد. در برنامههای ناهمگام قضیه متفاوت است- ممکن است شامل وقفههایی در اجرایشان باشند که در این وقفهها دیگر کدها میتوانند اجرا شوند.
اجازه بدهید تا به مثالی نگاه کنیم. یکی از سرگرمیهای کلاغهای ما این است که تعداد جوجههایی که از تخم بیرون می آیند در طول یک سال در روستا را بشمارند. لانهها این عدد را در بافتهای ذخیرهسازیشان حفظ می کنند. کد پیش رو تلاش میکند تا تمامی اعداد موجود در همهی لانهها را برای یک سال مشخص بشمارد.
function anyStorage(nest, source, name) { if (source == nest.name) return storage(nest, name); else return routeRequest(nest, source, "storage", name); } async function chicks(nest, year) { let list = ""; await Promise.all(network(nest).map(async name => { list += `${name}: ${ await anyStorage(nest, name, `chicks in ${year}`) }\n`; })); return list; }
قسمت <bdo>
async name =>
</bdo>
نشان می دهد که توابع پیکانی arrow functions را همچنین میتوان به صورت async
با قرار دادن واژهی async
در ابتدای آن ایجاد کرد.
کد ما در نگاه اول نادرست به نظر نمیرسد... تابع پیکانی async
بر روی مجموعهی لانهها نگاشت میشود، آرایهای از promiseها تولید میشود و سپس از <bdo>
Promise.all
</bdo>
برای انتظار برای همهی اینها قبل از بازگشتن از لیستی که میسازند استفاده میشود.
اما این کد مطمئنا مشکل دارد. خروجی آن همیشه لانهای است که کندترین پاسخ را داشته است.
chicks(bigOak, 2017).then(console.log);
میتوانید علت این مشکل را بیابید؟
مشکل در قسمت عملگر <bdo>
+=
</bdo>
قرار دارد، که مقدار فعلی لیست را در زمانی که دستور شروع به اجرا میکند میگیرد و بعد از اینکه دستور await
به پایان میرسد، متغیر list
را معادل با آن مقدار به اضافه رشتهی افزوده شده قرار می دهد.
اما در این میان جایی که دستور شروع به اجرا میکند و زمانی که به اتمام میرسد یک وقفهی ناهمگام وجود دارد. عبارت map
قبل از اینکه چیزی به لیست اضافه شود، اجرا می شود بنابراین هرکدام از عملگرهای <bdo>
+=
</bdo>
با یک رشتهی خالی شروع می کنند و به پایان می رسند، زمانی که بازیابی مخزنش به اتمام برسد، متغیر list
را برابر با یک لیست تک-خطی قرار می دهد — نتیجه افزودن خطش به رشتهی تهی.
بجای اینکه لیست را با تغییر یک متغیر بسازیم، با برگرداندن خطوط از promiseهای نگاشت شده و فراخوانی join
روی نتیجهی <bdo>
Promise.all
</bdo>
، میتوان به سادگی از این اشکال جلوگیری کرد. به طور معمول، محاسبهی مقدارهای جدید نسبت به تغییر مقادیر فعلی کمتر خطاساز هستند.
async function chicks(nest, year) { let lines = network(nest).map(async name => { return name + ": " + await anyStorage(nest, name, `chicks in ${year}`); }); return (await Promise.all(lines)).join("\n"); }
اشتباهاتی شبیه این خیلی ساده اتفاق می افتند مخصوصا زمانی که از await
استفاده می کنیم، و باید حواستان به جایی که وقفهها در کدتان رخ می دهد باشد. یک مزیت برنامه نویسی ناهمگام (چه با استفاده از callbackها ، promise ها یا await) به صورت صریح در جاوااسکریپت، این است که پیدا کردن این وقفهها نسبتا ساده است.
خلاصه
برنامهنویسی ناهمگام این امکان را فراهم می سازد که بتوان برای کارهای اجرایی زمانگیر صبر کرد بدون اینکه برنامه در حین انجام این کارها متوقف شود. محیطهای جاوااسکریپت نوعا این سبک از برنامهنویسی را با استفاده از callback ها پیاده سازی می کنند، توابعی که بعد از پایان یافتن کارهای مورد نظر، فراخوانی میشوند. یک حلقهی رخداد، این توابع callback را زمانبندی میکند تا در زمان مناسب فراخوانی شوند، یکی پس از دیگری، تا اجرای آنها با تداخل روبرو نشود.
برنامهنویسی ناهمگام با استفاده از promise ها آسان تر میشود، اشیائی که نمایندهی کارهایی هستند که ممکن است در آینده تکمیل شوند، و توابع async
، که به شما این امکان را میدهند تا یک برنامهی ناهمگام را به شکلی بنویسید که انگار همگام است.
تمرینها
رهگیری چاقوی جراحی
کلاغهای روستا یک چاقوی جراحی قدیمی دارند که گاهی اوقات از آن برای ماموریتهای خاص استفاده میکنند — فرض کنید، برای بریدن توری درها یا بسته ها. برای اینکه بتوان به سرعت آن را رهگیری کرد، هربار که چاقو به لانهی دیگری منتقل می شد، یک مدخل به مخزن هر دو لانه اضافه می شد، لانهای که آن را داشت و لانهای که آن را دریافت کرده است و این مدخل با نام "scalpel"
و با مقداری برای محل جدیدش ذخیره میشود.
این به این معنا است که برای پیدا کردن چاقو باید به تاریخچهی نشانههای موجود در مخزن مراجعه کرد تا اینکه به لانهای برسید که به خودش ارجاع می دهد.
یک تابع async
به نام locateScalpel
ایجاد کنید که این کار را انجام می دهد که از لانهای که روی آن اجرا میشود شروع میکند. میتوانید از تابع anyStorage
که پیش تر تعریف شده برای دسترسی به لانههای مورد نظر استفاده کنید. چاقو از مدت زمان مدیدی است که بین لانهها دست به دست میشود که میتوان نتیجه گرفت که هر لانه یک مدخل "scalpel"
را در مخزنش دارد.
در گام بعدی، همین تابع را بدون استفاده از async
و await
بنویسید.
آیا شکستهای درخواستها به درستی به عنوان عدم پذیرش یک promise برگردانده شده، در هر دو نسخه نمایش داده میشوند؟ چگونه؟
async function locateScalpel(nest) { // Your code here. } function locateScalpel2(nest) { // Your code here. } locateScalpel(bigOak).then(console.log); // → Butcher Shop
این کار را میتوان به وسیلهی یک حلقه که درون لانهها را میگردد صورت داد، اگر مقداری مطابق نام لانهی فعلی پیدا کند آن را برمی گرداند و درغیر این صورت به سراغ لانهی بعدی می رود. در تابع async
، یک دستور for
یا while
میتواند استفاده شود.
برای انجام این کار در یک تابع ساده، باید حلقهی خودتان را به وسیلهی یک تابع بازگشتی بنویسید. آسان ترین روش انجام این کار این است که تابع یک promise را با فراخوانی then
روی promiseای که مقدار ذخیرهشده را برمیگرداند بنویسید. بسته به اینکه آن مقدار با نام لانهی فعلی مطابقت داشته باشد یا خیر ، تابع گرداننده، یا آن مقدار را برمیگرداند یا یک promise دیگر با فراخوانی دوبارهی تابع برمیگرداند.
فراموش نکنید که حلقه را با یک بار فراخوانی تابع بازگشتی از درون تابع اصلی شروع کنید.
در تابع async
، promiseهای رد شده به وسیلهی await
به استثنا تبدیل میشوند. زمانی که یک تابع async
یک استثنا تولید میکند، promise آن رد شده است.
اگر تابع را بدون استفاده از async
همانطور که مشخص شده است پیاده سازی کنید، نحوهی عملکرد پیشفرض then
نیز باعث تولید یک شکست برای پایان دادن promise برگشتی میشود. اگر درخواستی با شکست روبرو شود، گردانندهای که به then
ارسال میشود، فراخوانی نمیگردد و promiseای که برمیگرداند به همان دلیل رد میشود.
ساختن Promise.all
با داشتن یک آرایه از promise ها، متد <bdo>
Promise.all
</bdo>
یک promise را برمی گرداند که برای همهی promise های موجود در آرایه، منتظر می ماند تا پایان یابند. در صورت موفقیت، آرایهای از مقدارهای نتایج تولید میشود. اگر یک promise موجود در آرایه با شکست روبرو شود، promise ای که به وسیله all
برگردانده میشود نیز با شکست روبرو میشود، همراه با دلیل شکست promise مشکل خورده.
تابعی به نام Promise_all
بنویسید که همین کار را انجام دهد.
به خاطر داشتهباشید که بعد از اینکه یک promise موفق شود یا با شکست روبرو شود، دیگر نمیتواند دوباره موفق یا شکست بخورد و فراخوانیهای بعدی به توابعی که برای نتیجهیابی آن اقدام می کنند صرف نظر میشوند. این میتواند راهی که شما شکستها را در promise تان رسیدگی میکنید ساده تر سازد.
function Promise_all(promises) { return new Promise((resolve, reject) => { // Your code here. }); } // Test code. Promise_all([]).then(array => { console.log("This should be []:", array); }); function soon(val) { return new Promise(resolve => { setTimeout(() => resolve(val), Math.random() * 500); }); } Promise_all([soon(1), soon(2), soon(3)]).then(array => { console.log("This should be [1, 2, 3]:", array); }); Promise_all([soon(1), Promise.reject("X"), soon(3)]) .then(array => { console.log("We should not get here"); }) .catch(error => { if (error != "X") { console.log("Unexpected failure:", error); } });
تابعی که به سازندهی Promise
داده میشود نیاز خواهد داشت که then
را روی هر یک از promiseهای آرایه فراخوانی کند. زمانی که یکی از این promiseها موفق شود، دو چیز لازم است تا اتفاق بیفتد. مقدار نتیجه باید در آرایه نتیجه و در موقعیت صحیح ذخیره شود، و باید بررسی کنیم که اگر این promise آخرین promise در حال بررسی بود، promise خودمان را به پایان برسانیم.
عمل آخر را میتوان به وسیلهی یک شمارنده استفاده کنیم که مقدار اولیهاش از اندازهی آرایه شروع میشود و با هر بار موفقیت یک promise، 1 واحد کاهش مییابد. وقتی این شمارنده به عدد 0 رسید، کار تمام است. مطمئن شوید که خالی بودن آرایهی ورودی را نیز بررسی کرده باشید ( که در این صورت هیچ promiseی حل و فصل نخواهد شد).
مدیریت شکست نیاز به کمی تفکر دارد اما درنهایت کاری بسیار ساده است. کافی است تابع reject
متعلق به promise پوشش دهنده را به هر یک از promiseهای موجود در آرایه به عنوان گردانندهی catch
ارسال کنید یا به عنوان آرگومان دوم به then
بفرستید درنتیجه شکست در یکی از آن دو منجر به رد شدن کل promise پوشش دهنده میشود.