Promiseها در جاوااسکریپت – بخش چهارم (آخر)

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

نمودار عملکرد Promiseها در جاوااسکریپت

Promiseهای موازی و سری؛ ترکیب هر دو برای نتیجه‌ی بهتر

نوشتن Promiseها در جاوااسکریپت و تفکر غیر همزمان ساده نیست. اگر برای شما نیز این نوع تفکر سخت است، ابتدا کد خود را به شکل همزمان (به انگلیسی synchronous) تصور کنید. برای مثال:

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("Eyval... hamash load shod.");
}
catch (err) {
  addTextToPage("Ufff, Khata: " + err.message);
}

document.querySelector('.spinner').style.display = 'none';

این کد به خوبی کار خواهد کرد. اما به شکل همزمان است و تا زمان لود شدن کامل محتوا، مرورگر را مشغول و قفل می‌کند. راه دیگر نوشتن کد بالا استفاده از روش‌های غیر همزمان و متد then() است.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Baraye har fasl bayad yek methode then() benevisim.
}).then(function() {
  // va dar akharin then() :
  addTextToPage("Eyval... hamash load shod.");
}).catch(function(err) {
  // gereftane hameye khatahaye masir.
  addTextToPage("Ufff, Khata: " + err.message);
}).then(function() {
  // penhan kardane spinner:
  document.querySelector('.spinner').style.display = 'none';
})

برای کد بالا، باید راهی پیدا کنیم که نیاز نباشد برای لود کردن هر فصل یک then() بنویسیم. آیا باید مشابه شکل همزمان، از forEach() استفاده کنیم؟

story.chapterUrls.forEach(function(chapterUrl) {
  // gereftane fasl:
  getJSON(chapterUrl).then(function(chapter) {
    // ezafe kardan be safhe:
    addHtmlToPage(chapter.html);
  });
})

متد forEach() برای کار غیر همزمان نیست. پس اگر کد را به شکل بالا بنویسیم، هرکدام از فصل‌ها که زودتر دانلود شد، زودتر نمایش داده می‌شود که این مطابق میل ما نیست. پس باید این مشکل را مرتفع کنیم؛ اما چگونه؟

Promiseها در جاوااسکریپت و ساختن توالی (sequence)

اگر بخواهیم آرایه‌ی chapterUrl خود را به توالی‌ای از Promiseها تبدیل کنیم که یکی پس از دیگری دانلود شوند، به شکل زیر عمل می‌کنیم:

// baa yek Promise shoru mikonim ke hamishe resolve mishavad:
var sequence = Promise.resolve();

// yek loop baraye chapterUrl minevisim:
story.chapterUrls.forEach(function(chapterUrl) {
  // loade har fasl raa be entehaye Promise michasbaanim:
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

اولین بار است که Promise.resolve() را مشاهده می‌کنیم! این دستور پرامیسی را می‌سازد که با هر مقداری که به آن بدهیم Resolve می‌شود. اگر یک instance از این Promise تعریف کنیم (آن را new کنیم)، انگار پرامیسی تعریف کرده‌ایم که اجرا شده، موفق شده و مقدار نهایی را برگردانده است. اگر مقداری که به آن می‌دهیم یک عمل غیر همزمان یا شبه-پرامیس باشد (یعنی متد then() داشته باشد)، این دستور یک Promise مشابه پرامیس‌های معرفی شده در بخش‌های قبل را می‌سازد که موفق یا رد می‌شود. اگر هم مقدار ساده‌ای به آن داده شود، همان را برمی‌گرداند. مثلا Promise.resolve('Hello') مانند یک پرامیس موفق شده‌ای عمل می‌کند که مقدار برگردانده شده از آن 'Hello' است. یا خود Promise.resolve() مانند پرامیسی عمل می‌کند که اجرا و موفق شده و مقداری که با آن موفق یا fulfill می‌شود، undefined است. به طور مشابه، Promise.reject(val) را نیز داریم که پرامیسی می‌سازد که با مقدار داده شده به آن ریجکت می‌شود.

حالا می‌توانیم کد بالا را با استفاده از متد reduce() برای آرایه‌های جاوااسکریپت، به شکل جمع و جورتری نیز بنویسیم. اگر با متد reduce() آشنا نیستید، مقاله‌ی من تحت عنوان ۱۰ متد مفید جاوااسکریپت در رابطه با آرایه‌ها را مطالعه کنید.

story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // loade har fasl raa be entehaye Promise michasbaanim:
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

خب بیایید همه چیز را به هم چسبانده و سر و سامان دهیم:

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // loade har fasl raa be entehaye Promise michasbaanim:
    return sequence.then(function() {
      // dowloade fasl:
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // ezafe kardane fasl be safhe:
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // vaghti hameye fasl-ha load shodand:
  addTextToPage("Eyval... hamash load shod.");
}).catch(function(err) {
  // gereftane hameye khatahaye masir.
  addTextToPage("Ufff, Khata: " + err.message);
}).then(function() {
  // penhan kardane spinner:
  document.querySelector('.spinner').style.display = 'none';
})

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

نحوه‌ی لود شدن Promiseها در حالت توالی

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

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

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

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // arayeye promise-ha ra migirim:
  return Promise.all(
    // Map kardane arayeye URL-ha be arayeye Promise-ha
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // hala fasl-ha raa be tartib darim ke baa loop chaap mikonim:
  chapters.forEach(function(chapter) {
    // …va afzoodane safhe:
    addHtmlToPage(chapter.html);
  });
  addTextToPage("Eyval... hamash load shod.");
}).catch(function(err) {
  // gereftane hameye khatahaye masir.
  addTextToPage("Ufff, Khata: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

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

نحوه‌ی لود شدن Promieها در حالت دانلود یکجا

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

برای انجام این کار ابتدا به طور یکجا، فایل‌های JSON خود را برای هر فصل دانلود می‌کنیم و بعدا در ادامه یک توالی ایجاد می‌کنیم تا آن‌ها را به صفحه اضافه کنیم:

getJSON('story.json')
.then(function(story) {
  addHtmlToPage(story.heading);

  // aarayeye URL-haye fasl-haa raa baa methode map()
  // be shekle aarayeye promise-haye loade json dar miavarim.
  // in masale baes mishavad download-ha yekja shoru shavad:
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // az reduce estefade mikonim taa zanjire dorost konim
      // va mohtavaye har fasl raa be safhe ezafe konim:
      return sequence
      .then(function() {
        // montazer mimanim taa harchizi dar zanjire OK shavad.
        // baadan baraye fasle feeli montazer mimanim.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("Eyval... hamash load shod.");
}).catch(function(err) {
  // gereftane hameye khatahaye masir.
  addTextToPage("Ufff, Khata: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

در این‌جا با map() کردن آرایه‌ی URLها، باعث می‌شویم همه‌ی درخواست‌های دانلود JSON فرستاده شوند. نتیجه، آرایه‌ای است از Promiseهایی که اجرا شده‌اند اما ممکن است هنوز در وضعیت settle قرار نگرفته باشند. به عبارتی دانلود هنوز کامل نشده باشد. در ادامه، مشابه توضیحات قبل، این آرایه‌ی Promiseها را به یک زنجیره پرامیس تبدیل کرده‌ایم. در خط ۱۶ که هر پرامیس را برمی‌گردانیم، اگر پرامیس settle نشده‌ باشد، منتظر می‌مانیم تا settle شود و اگر قبلا settle شده باشد بلافاصله به متد then() بعدی می‌رویم و نتیجه را چاپ می‌کنیم. بنابراین اگر فصل سوم زودتر دانلود شود، کد همچنان منتظر دانلود فصل دوم است. اما به محض دانلود شدن فصل دوم، فصل دوم چاپ شده و فصل سوم نیز بدون انتظار بلافاصله چاپ می‌شود. با این توضیحات انتظار نتیجه‌ای مشابه نتیجه‌ی زیر را داریم:

نحوه‌ی لود شدن Promiseها در حالت لود یکجا و نمایش زنجیره‌ای

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

دیدگاهتان را بنویسید

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