دليل تعلم جافاسكربت واجهة الوعود البرمجية Promise API في جافاسكربت


صفا الفليج

ثمّة 5 توابِع ثابتة (static) في صنف الوعود Promise. سنشرح الآن عن استعمالاتها سريعًا.

Promise.all

لنقل بأنّك تريد تنفيذ أكثر من وعد واحد في وقت واحد، والانتظار حتّى تجهز جميعها.

مثلًا أن تُنزّل أكثر من عنوان URL في آن واحد وتُعالج المحتوى ما إن تُنزّل كلها.

وهذا الغرض من وجود Promise.all. إليك صياغته:

let promise = Promise.all([...promises...]);

يأخذ التابِع Promise.all مصفوفة من الوعود (تقنيًا يمكن أن تكون أيّ … ولكنّها في العادة مصفوفة) ويُعيد وعدًا جديدًا.

لا يُحلّ الوعد الجديد إلّا حين تستقرّ الوعود في المصفوفة، وتصير تلك المصفوفة التي تحمل نواتج الوعود - تصير ناتج الوعد الجديد.

مثال على ذلك تابِع Promise.all أسفله إذ يستقرّ بعد 3 ثوان ويستلم نواتجه في مصفوفة [1, 2, 3]:

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert); // 1,2,3 ما إن تجهز الوعود يُعطي كلّ واحد عنصرًا في المصفوفة

لاحِظ كيف أنّ ترتيب عناصر المصفوفة الناتجة يتطابق مع ترتيب الوعود، فحتّى لو أخذ الوعد الأوّل وقتًا أطول من غيره حتّى يُحلّ، فسيظلّ العنصر الأول في مصفوفة النواتج.

نستعمل عادةً حيلة صغيرة أن نصنع مصفوفة جديدة بخارطة تأخذ القديمة وتحوّلها إلى وعود، ثمّ نمرّر ذلك كلّه إلى Promise.all.

فمثلًا لو لدينا مصفوفة من العناوين، يمكننا جلبها كلّها هكذا:

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

// ‫نحوّل كلّ عنوان إلى وعد التابِع fetch
let requests = urls.map(url => fetch(url));

// ‫ينتظر Promise.all حتّى تُحلّ كلّ المهام
Promise.all(requests)
  .then(responses => responses.forEach(
    response => alert(`${response.url}: ${response.status}`)
  ));

من أكبر الأمثلة على جلب المعلومات. هي جلب المعلومات لمجموعة من مستخدمي موقع GitHub من خلال اسمائهم (يمكننا جلب مجموعة من السلع بحسب رقم المعرف الخاص بها، منطق الحل متشابه في كِلا الحالتين):

let names = ['iliakan', 'remy', 'jeresig'];

let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));

Promise.all(requests)
  .then(responses => {
    // استدعيت جميع الردود بنجاح
    for(let response of responses) {
      alert(`${response.url}: ${response.status}`); // ‫أظهر 200 من أجل كلّ عنوان url
    }

    return responses;
  })
  // ‫اربط مصفوفة الردود مع مصفوفة response.json()‎ لقراءة المحتوى
  .then(responses => Promise.all(responses.map(r => r.json())))
  // ‫تحلل جميع الإجابات : وتكوّن مصفوفة "users" منهم
  .then(users => users.forEach(user => alert(user.name)));

لو رُفض أيّ وعد من الوعود، سيرفض الوعد الذي يُعيده Promise.all مباشرةً بذلك الخطأ.

مثال:

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  // هنا
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!

نرى هنا أنّ الوعد الثاني يُرفض بعد ثانيتين، ويؤدّي ذلك إلى رفض Promise.all كاملًا، وهكذا يُنفّذ التابِع ‎.catch ويصير الخطأ ناتج Promise.all كلّه.

تحذير: في حالة حدوث خطأ ما، تُتجاهل الوعود الأُخرى لو أن وعدًا ما رُفضَ، ستُرفض جميع الوعود من خلال تابع Promise.all مباشرةً، متناسيًا بذلك الوعود الأخرى في القائمة. وعندها ستُتجاهل نتائجها أيضًا.

على سبيل المثال، إن كان العديد من استدعاءات fetch كما هو موضح في المثال أعلاه، وفشِل أحدها، فسيستمر تنفيذ الاستدعاءات الأخرى. ولكن لن يشاهدها تابع Promise.all بعد الآن. ربما ستُنجز بقية الوعود، ولكن نتائجها ستُتجاهل في نهاية المطاف.

لن يُلغي التابع Promise.all الوعود الأخرى، إذ لا يوجد مثل هذا الفعل في الوعود، سنغطي في فصل آخر طريقة المتبعة في إلغاء الوعود وسيساعدنا التابع AbortController في ذلك، ولكنه ليس جزءًا من واجهة الوعود البرمجية.

ملاحظة: يسمح التابع Promise.all(iterable)‎ لغير الوعود (القيم النظامية) في iterable. يقبل التابع Promise.all(...)‎ وعودًا قابلة للتكرار (مصفوفات في معظم الوقت). ولكن لو لم تكُ تلك الكائنات وعودًا. فستُمرّر النتائج كما هي.

فمثلًا، النتائج هنا [1, 2, 3]:

Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  }),
  2,
  3  
]).then(alert); // 1, 2, 3

يمكننا تمرير القيم الجاهزة لتابِع Promise.all عندما يكون ذلك مناسبًا.

Promise.allSettled

إضافة حديثة هذه إضافة حديثة للغة، وقد يحتاج إلى تعويض نقص الدعم في المتصفحات القديمة.

لو رُفض أحد الوعود في Promise.all فسيُرفض كلّه مجتمعًا. هذا يفيدنا لو أردنا استراتيجية ”إمّا كل شيء أو لا شيء“، أي حين نطلب وجود النتائج كلها كي نواصل:

Promise.all([
  fetch('/template.html'),
  fetch('/style.css'),
  fetch('/data.json')
]).then(render); // تابِع التصيير يطلب نواتج كلّ توابِع الجلب

بينما ينتظر Promise.allSettled استقرار كلّ الوعود. للمصفوفة الناتجة هذه العناصر:

  • {status:"fulfilled", value:result} من أجل الاستجابات الناجحة.
  • ‎{status:"rejected", reason:error}‎ من أجل الأخطاء.

مثال على ذلك هو لو أردنا جلب معلومات أكثر من مستخدم واحد. فلو فشل أحد الطلبات ما زلنا نريد معرفة معلومات البقية.

فلنستعمل Promise.allSettled:

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { // (*)
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });

ستكون قيمة results في السطر (*) أعلاه كالآتي:

[
  {status: 'fulfilled', value: ...ردّ...},
  {status: 'fulfilled', value: ...ردّ...},
  {status: 'rejected', reason: ...كائن خطأ...}
]

هكذا نستلم لكلّ وعد حالته وقيمته أو الخطأ value/error.

تعويض نقص الدعم

لو لم يكن المتصفّح يدعم Promise.allSettled فمن السهل تعويض ذلك:

if(!Promise.allSettled) {
  Promise.allSettled = function(promises) {
    return Promise.all(promises.map(p => Promise.resolve(p).then(value => ({
      state: 'fulfilled',
      value
    }), reason => ({
      state: 'rejected',
      reason
    }))));
  };
}

في هذه الشيفرة يأخذ التابِع promises.map قيم الإدخال ويحوّلها إلى وعود (في حال وصله شيء ليس بالوعد) ذلك باستعمال p => Promise.resolve(p)‎، بعدها يُضيف دالة المُعاملة ‎.then لكلّ وعد.

هذه الدالة تحوّل النواتج الناجحة v إلى {state:'fulfilled', value:v}، والأخطاء r إلى {state:'rejected', reason:r}. هذا التنسيق الذي يستعمله Promise.allSettled بالضبط.

يمكننا لآن استعمال Promise.allSettled لجلب نتائج كلّ الوعود الممرّرة حتّى لو رفضت بعضها.

Promise.race

يشبه التابِع Promise.all إلّا أنّه ينتظر استقرار وعد واحد فقط ويأخذ ناتجه (أو الخطأ). صياغته هي:

let promise = Promise.race(iterable);

فمثلًا سيكون الناتج هنا 1:

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

أوّل الوعود هنا كان أسرعها وهكذا صار هو الناتج. ومتى ”فاز أوّل وعد مستقرّ بالسباق“، تُهمل بقية النواتج/الأخطاء.

Promise.resolve/reject

نادرًا ما نستعمل Promise.resolve و Promise.reject في الشيفرات الحديثة إذ صياغة async/await (نتكلم عنها في مقال لاحق)

نشرحها هنا ليكون شرحًا كاملًا، وأيضًا لمن لا يستطيع استعمال async/await لسبب أو لآخر.

  • Promise.resolve(value)‎ يُنشئ وعد مُنجز بالناتجvalue.

ويشبه:

let promise = new Promise(resolve => resolve(value));

يستخدم هذا التابع للتوافقية، عندما يُتوقع من تابع ما أن يُعيد وعدًا.

فمثلًا، يجلب التابِع أدناه loadCached عنوان URL ويخزن المحتوى في الذاكرة المؤقتة. ومن أجل الاستدعاءات اللاحقة لنفس عنوان URL سيحصل على المحتوى فورًا من الذاكرة المؤقتة، ولكنه يستخدم التابع Promise.resolve لتقديم وعدًا بهذا الأمر. لتكون القيمة المرتجعة دائمًا وعدًا:

let cache = new Map();

function loadCached(url) {
  if (cache.has(url)) {

    return Promise.resolve(cache.get(url)); // (*)

  }

  return fetch(url)
    .then(response => response.text())
    .then(text => {
      cache.set(url,text);
      return text;
    });
}

يمكننا كتابة loadCached(url).then(…)‎ لأننا نضمن أن التابِع سيُعيد وعدًا. كما يمكننا دائمًا استخدام ‎.then بعد تابِع loadCached. وهذا هو الغرض من Promise.resolve في السطر (*).

Promise.reject

  • Promise.reject(error)‎ ينشئ وعد مرفوض مع خطأ. ويشبه:
let promise = new Promise((resolve, reject) => reject(error));

عمليًا لا نستعمل هذا التابِع أبدًا.

خلاصة

لصنف الوعود Promise خمس توابِع ثابتة:

  1. Promise.all(promises)‎ -- ينتظر جميع الوعود لتعمل ويعيد مصفوفة بنتائجهم. وإذا رُفض أي وعدٍ منهم، سيرجع Promise.all خطأً، وسيتجاهلُ جميع النتائج الأخرى.
  2. Promise.allSettled(promises)‎ -- (التابع مضاف حديثًا) ينتظر جميع الوعود لتُنجز ليُعيد نتائجها كمصفوفة من الكائنات تحتوي على: -state: الحالة وتكون إما "fulfilled" أو "rejected". -value: القيمة (إذا تحقق الوعد) أو reason السبب (إذا رُفض).
  3. Promise.race(promises)‎ -- ينتظر الوعد الأول ليُنجز وتكون نتيجته أو الخطأ الذي سيرميه خرج هذا التابِع.
  4. Promise.resolve(value)‎ -- ينشئ وعدًا منجزًا مع القيمة الممرّرة.
  5. Promise.reject(error)‎ -- ينشئ وعدًا مرفوضًا مع الخطأ المُمرّر.

ومن بينها Promise.all هي الأكثر استعمالًا عمليًا.

ترجمة -وبتصرف- للفصل Promise API من كتاب The JavaScript language





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن