Chapter 11برنامه‌نویسی ناهمگام

چه کسی می تواند تا ته‌نشین شدن گل‌های آب، شکیبا باشد؟ چه‌کسی می تواند بی‌حرکت بماند تا لحظه‌ی درست عمل فرا برسد؟

لائودْزی, دائو ده جینگ
Picture of two crows on a branch

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

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

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

ناهمگامی

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

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

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

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

راه حل این مسئله، در یک سیستم همگام، استفاده از نخ‌های (threads) اضافی کنترل است. یک thread یک برنامه‌ی دیگر است که در حال اجرا است که اجرای آن ممکن است توسط سیستم عامل بین برنامه‌های دیگر قرار گیرد – چون بیشتر کامپیوترهای مدرن دارای چندین پردازشگر هستند، چندین thread را می توان در یک آن روی پردازشگرها اجرا کرد. یک thread دیگر می تواند درخواست دوم را شروع کند و سپس هر دوی thread ها منتظر نتیجه‌ی درخواستشان می مانند که بعد از آن دوباره همگام شده و نتایج را باهم ترکیب می کنند.

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

Control flow for synchronous and asynchronous programming

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

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

هر دو پلتفرم مهم برنامه‌نویسی جاوااسکریپت – مرورگرها و Node.js – عملیاتی که ممکن است زمانگیر باشند را به صورت ناهمگام اجرا می کنند و از نخ‌ها (threads) استفاده نمی کنند. به دلیل اینکه برنامه‌نویسی روی thread ها کار سختی محسوب می شود (در این نوع برنامه‌نویسی درک کارکرد برنامه، به دلیل انجام چند کار در آن واحد بسیار سخت‌تر می شود)، روش ناهمگام عموما چیز خوبی محسوب می شود.

فناوری کلاغ‌ها

خیلی از مردم می‌دانند که کلاغ‌ها پرنده‌هایی بسیار باهوش هستند. آن ها می توانند از ابزار استفاده کنند، برای آینده برنامه ریزی کنند، چیزهایی را به خاطر بسپارند و حتی این موارد را با هم به اشتراک بگذارند.

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

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

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

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

دوست متخصص کلاغ ما نقشه‌ای از شبکه‌ی لانه‌های کلاغ‌ها در روستای Hières-sur-Amby قرار دارد، کشیده است که در حاشیه‌ی رودخانه‌ی Rhône قرار دارد. آن نقشه نشان می دهد که لانه‌ها و ارتباطاتشان چگونه است:

A network of crow nests in a small village

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

توابع callback

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

به عنوان یک مثال، تابع setTimeout، که در Node.js و مرورگرها در دسترس است، به اندازه‌ی هزارم ثانیه‌ ای که مشخص شده است منتظر می ماند و سپس یک تابع را فراخوانی می کند.

setTimeout(() => console.log("Tick"), 500);

این انتظار معمولا خیلی کاربردهای مهمی ندارد؛ اما گاهی تواند مفید باشد مانند به‌روز‌رسانی یک انیمیشن یا بررسی طول کشیدن چیزی در برنامه.

اجرای چندین عمل ناهمگام در یک ردیف با استفاده از توابع callback به این معنا است که شما باید به ارسال توابع جدید برای ادامه‌ی محاسبه بعد از هر عمل ادامه دهید.

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

بافت‌های ذخیره‌سازی، بخش‌های اطلاعات را که به فرمت JSON درآمده اند، تحت نام‌هایی ذخیره می کنند. یک کلاغ ممکن است اطلاعاتی در مورد مخفیگاه‌ غذاها را به عنوان "food caches" ذخیره کند، که می تواند دارای آرایه‌ای از نام‌هایی باشد که به دیگر بخش‌های اطلاعات اشاره می نمایند، اطلاعاتی که مخفیگاه واقعی را توصیف می کنند. برای جستجوی یک مخفیگاه غذا در بافت‌های ذخیره‌سازی لانه‌ی Big Oak، یک کلاغ می تواند کدی مثل زیر را اجرا کند.

import {bigOak} from "./crow-tech";

bigOak.readStorage("food caches", caches => {
  let firstCache = caches[0];
  bigOak.readStorage(firstCache, info => {
    console.log(info);
  });
});

(تمامی متغیرها و رشته‌ها از زبان کلاغی به زبان انگلیسی ترجمه شده اند.)

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

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

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

رابطی که توسط ماژول "./crow-tech" صادر می شود، تابعی دارای 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" اضافه می می‌شود که در واقع تنها یک یادداشت را به لانه‌ی داده شده ارسال می کند. پیاده‌سازی ما، از console.log برای تایید رسیدن درخواست استفاده می کند. لانه‌ها دارای خاصیتی به نام name هستند که نامشان را نگه داری می کند.

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

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

Promise ها

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

این دقیقا چیزی است که کلاس استاندارد Promise انجام می دهد. یک promise یک عمل ناهمگام است که در زمانی تکمیل می شود و مقداری را تولید می کند. می تواند هرکسی که علاقمند باشد را در زمان آماده شدن مقدارش باخبر کند.

آسان ترین روش ایجاد یک promise فراخوانی Promise.resolve است. این تابع اطمینان حاصل می کند که مقداری که به آن می دهید درون یک 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 بعدی جریان می یابد. تابعی به نام Promise.reject وجود دارد که یک 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);
    }
  });
}

Promise.resolve برای تبدیل مقدار بازگشتی از handler به یک promise استفاده می شود؛ اگر قبلا انجام نشده باشد.

توجه داشته باشید فراخوانی به تابع handler بایستی درون یک بلاک try قرار می گرفت، تا اطمینان حاصل شود هر استثنایی که تولید می کند مستقیما به تابع callback داده می شود. این به خوبی سختی رسیدگی درست به خطاها در مدل callback های خام را نشان می دهد – به راحتی می ممکن است مدیریت صحیح استثناها را فراموش کنیم؛ مانند بالا و اگر این کار را انجام ندهید، شکست‌ها به callback درستی گزارش نمی شوند. در promise ها، این کار را به طور خودکار انجام می می‌شود بنابراین کمتر خطاساز خواهند بود.

مجموعه‌ای از promise ها

هر کامپیوتر لانه دارای آرایه‌ای از دیگر لانه‌ها می‌باشد که درون محدوده‌ی مخابره قرار دارند و آن را در خاصیت neighbors آن ذخیره می کند. برای بررسی اینکه کدام یک از آن ها در حال حاضر در دسترس هستند، می توانید تابعی بنویسید که تلاش کند تا یک درخواست “ping” (درخواستی که فقط برای دریافت پاسخ ارسال می شود) را به هر یک از لانه ها ارسال کند و مشاهده کنید کدام درخواست پاسخ داده می شود.

زمانی که با مجموعه‌ای از promise ها کار می کنید که در یک زمان یکسان اجرا می شوند، تابع Promise.all می تواند مفید باشد. این تابع یک promise را برمی گرداند که برای همه‌ی promise های درون آرایه صبر می کند تا به نتیجه برسند و بعد نتیجه را درون یک آرایه از مقدارهایی که این promise ها تولید کرده اند می ریزد (به همان ترتیبی که در آرایه‌ی اصلی آمده بودند). اگر یک promise رد شده باشد، نتیجه‌ی Promise.all خودش نیز رد می شود.

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);
});

در مقایسه از JSON.stringify استفاده می شود چرا که ==، روی اشیاء و آرایه‌ها، فقط زمانی 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 است، Object.keys روی آن جواب نمی دهد. متد keys در این شیء هم وجود دارد اما یک شمارنده را برمی گرداند نه یک آرایه. یک شمارنده (یا مقدار شمارنده) را می توان به وسیله‌ی Array.from به آرایه تبدیل کرد.

حتی با وجود استفاده از 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 ها.

زمانی که تابعی را با function* (یک ستاره بعد از کلیدواژه‌ی 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;
}

قسمت async name => نشان می دهد که توابع پیکانی arrow functions را همچنین می توان به صورت async با قرار دادن واژه‌ی async در ابتدای آن ایجاد کرد.

کد ما در نگاه اول نادرست به نظر نمی‌رسد... تابع پیکانی async بر روی مجموعه‌ی لانه‌ها نگاشت می شود، آرایه‌‌ای از promiseها تولید می شود و سپس از Promise.all برای انتظار برای همه‌ی این‌ها قبل از بازگشتن از لیستی که می‌سازند استفاده می شود.

اما این کد مطمئنا مشکل دارد. خروجی آن همیشه لانه‌ای است که کند‌ترین پاسخ را داشته است.

chicks(bigOak, 2017).then(console.log);

می توانید علت این مشکل را بیابید؟

مشکل در قسمت عملگر += قرار دارد، که مقدار فعلی لیست را در زمانی که دستور شروع به اجرا می کند می گیرد و بعد از اینکه دستور await به پایان می رسد، متغیر list را معادل با آن مقدار به اضافه رشته‌ی افزوده شده قرار می دهد.

اما در این میان جایی که دستور شروع به اجرا می کند و زمانی که به اتمام می رسد یک وقفه‌ی ناهمگام وجود دارد. عبارت map قبل از اینکه چیزی به لیست اضافه شود، اجرا می شود بنابراین هرکدام از عملگرهای += با یک رشته‌ی خالی شروع می کنند و به پایان می رسند، زمانی که بازیابی مخزنش به اتمام برسد، متغیر list را برابر با یک لیست تک-خطی قرار می دهد — نتیجه افزودن خطش به رشته‌ی تهی.

بجای اینکه لیست را با تغییر یک متغیر بسازیم، با برگرداندن خطوط از promiseهای نگاشت شده و فراخوانی join روی نتیجه‌ی Promise.all، می توان به سادگی از این اشکال جلوگیری کرد. به طور معمول، محاسبه‌ی مقدارهای جدید نسبت به تغییر مقادیر فعلی کمتر خطاساز هستند.

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 ها، متد Promise.all یک 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 پوشش دهنده می شود.