وعود JavaScript: مقدمة

تعمل الوعود على تبسيط العمليات الحسابية المؤجَّلة وغير المتزامنة. الوعد يمثل عملية لم تكتمل بعد.

مطورو البرامج، جهز نفسك لحظة محورية في تاريخ لتطوير مواقع الويب.

[بداية طبل الطبل]

وصلت الوعود إلى لغة JavaScript!

[تنفجر الألعاب النارية، وتتساقط الأمطار على الورق اللامع من أعلى الحشود]

في هذه المرحلة، تندرج ضمن إحدى هذه الفئات:

  • الأشخاص يحتفلون من حولك، لكنك لست متأكدًا من سبب الإزعاج عنه. ربما لست متأكدًا حتى من "الوعد" الموجودة. تهز كتفي، لكن الورق اللامع ثقيل على كتفك. إذا كان الأمر كذلك، فلا للقلق حيال هذا الأمر، استغرق الأمر وقتًا طويلاً لمعرفة سبب أهمية هذا الأمر باستمرار. وقد تحتاج إلى البدء من البداية.
  • لقد خرجت الكرة! أليس كذلك؟ لقد استخدمت مزايا التعهد بتقديم هذه من قبل ولكن يزعجك أن جميع عمليات التنفيذ لها واجهة برمجة تطبيقات مختلفة قليلاً. ما هي واجهة برمجة التطبيقات الخاصة بإصدار JavaScript الرسمي؟ ربما ترغب في بدء باستخدام المصطلحات.
  • لقد علمت بهذا بالفعل وأنت تسخر من أولئك الذين يقفزون مثل تلك الأخبار بالنسبة إليهم. توقف لحظة للاستمتاع بتفوقك، ثم نتوجه مباشرةً إلى مرجع واجهة برمجة التطبيقات.

توافُق المتصفّح ونظام polyfill

دعم المتصفح

  • Chrome: 32.
  • الحافة: 12.
  • Firefox: 29.
  • Safari: 8-

المصدر

تحسين أداء المتصفّحات التي لا تتوافق مع الوعود الكاملة بالامتثال أو إضافة وعود إلى المتصفحات الأخرى وNode.js، فتحقق من the polyfill (2k مضغوط ببرنامج gzip).

ما هو سبب الضجيج؟

جافا سكريبت عبارة عن سلسلة محادثات واحدة، مما يعني أنه لا يمكن تشغيل وحدتين من البرنامج النصي في نفس الوقت؛ يجب أن يعملوا واحدًا تلو الآخر. في المتصفحات، يمكن استخدام JavaScript سلسلة تتضمن مجموعة من الأشياء الأخرى التي تختلف عن المتصفح إلى المتصفح. ولكن عادةً ما تكون لغة JavaScript في قائمة الانتظار نفسها مثل الرسم وتحديث والأنماط والتعامل مع إجراءات المستخدم (مثل تمييز النص والتفاعل باستخدام عناصر التحكم في النموذج). يؤدي النشاط في أحد هذه الأمور إلى تأخير الآخرين.

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

من المحتمل أنّك استخدمت الأحداث وعمليات معاودة الاتصال للتحايل على هذه المشكلة. إليك الأحداث:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

هذا لا يعطس على الإطلاق. نحصل على الصورة ونضيف بعض المستمعين، ثم يمكن أن تتوقّف لغة JavaScript عن التنفيذ إلى أن يتم استدعاء أحد تلك المستمعات.

ولسوء الحظ، في المثال أعلاه، من الممكن أن تكون الأحداث قد وقعت قبل أن نبدأ الاستماع إليهم، لذلك نحن بحاجة إلى التغلب على ذلك باستخدام و"الاكتمال" خاصية الصور:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

لا يؤدي هذا الإجراء إلى رصد الصور التي بها أخطاء قبل أن يتسنى لنا الاستماع إليها لا يسمح لنا نموذج كائن المستند (DOM) بتنفيذ ذلك. أيضًا، هذه هي يتم الآن تحميل صورة واحدة. تزداد الأمور تعقيدًا إذا أردنا أن نعرف موعد مجموعة تم تحميل من إجمالي من الصور.

الأحداث ليست دائمًا أفضل طريقة

الأحداث رائعة بالنسبة للأشياء التي يمكن أن تحدث عدة مرات في نفس مثل "keyup" و"touchstart" وما إلى ذلك. بالنسبة إلى هذه الأحداث، لا داعي للقلق. حول ما حدث قبل إرفاق المستمع. ولكن عندما يتعلق الأمر نجاح/إخفاق غير متزامن، ومن الناحية المثالية، تحتاج إلى شيء مثل هذا:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

هذه هي النتائج التي تعد بها الوعود، ولكن مع تسمية أفضل. إذا كان لعناصر صورة HTML "جاهز" وعدنا بذلك، فيمكننا القيام بذلك:

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

في الأساس، تشبه الوعود إلى حد ما مستمعي الأحداث باستثناء:

  • لا يمكن أن ينجح الوعد أو يفشل مرة واحدة فقط. لا يمكن أن تنجح أو تفشل مرتين، ولا يمكنها أيضًا التبديل من النجاح إلى الفشل أو العكس بالعكس.
  • إذا نجح الوعد أو لم ينجح وأضفت لاحقًا نجاحًا أو فشلاً فسيتم استدعاء رد الاتصال الصحيح، على الرغم من أن الحدث استغرق مكان سابق.

ويعد هذا أمرًا مفيدًا للغاية للنجاح/الإخفاق غير المتزامن، لأنك أقل مهتمون بالوقت الدقيق الذي أصبح فيه أحد الأشياء متاحًا، وأكثر اهتمامًا في التفاعل مع النتيجة.

المصطلحات الواعدة

اختبار دومينيك دينيكولا يُرجى قراءة المسودة الأولى من هذه المقالة وقيّمتني "F" عن المصطلحات. لقد وضعني في السجن أجبرتُ على نسخ الحالات والمصير 100 مرة، وكتبت خطابًا قلقًا لوالديّ. على الرغم من ذلك، لا أزال خلط الكثير من المصطلحات، ولكن إليك الأساسيات:

يمكن أن يكون الوعد:

  • fulful - تم تنفيذ الإجراء المتعلق بالوعد بنجاح
  • مرفوض - تعذّر الإجراء المتعلق بالوعد
  • في انتظار الموافقة - لم يتمّ توصيلها أو رفضها بعد
  • settled - تم استيفاء الطلب أو رفضه.

المواصفات أيضًا المصطلح thenable لوصف كائن يشبه الوعد، في أنه يحتوي على طريقة then. يذكّرني هذا المصطلح بمباريات كرة القدم في إنجلترا المدير Terry Venables لذلك سأستخدمه بأقل قدر ممكن.

وصول وعود باستخدام JavaScript

وظلت وعود هذه متاحة منذ فترة في شكل مكتبات، مثل:

لكل من الوعود المذكورة أعلاه وJavaScript سلوكًا مشتركًا وموحّدًا يُسمى Promises/A+ في حال حذف فأنت تستخدم jQuery، وله شيء مشابه يسمى مؤجلة: ومع ذلك، يُرجى ملاحظة أنّ المؤجَّلات لا تتوافق مع الوعود أو +A، ما يجعلها مختلفة بعض الشيء وأقل فائدة لذا كن حذرًا. يحتوي jQuery أيضًا على نوع الوعد، ولكن هذه مجرد مجموعة فرعية من "المؤجلة" ولديها نفس المشكلات.

على الرغم من أن تنفيذ الوعد يتبع سلوكًا معياريًا، إلا أن بشكل عام. تتشابه وعود JavaScript في واجهة برمجة التطبيقات مع RSVP.js. في ما يلي كيفية إنشاء وعد:

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

تستخدم الدالة الإنشائية الوعد وسيطة واحدة، وهي استدعاء ذو معاملين، الحل والرفض. تنفيذ إجراء داخل رد الاتصال، وربما يكون غير متزامن، ثم الاتصال حل إذا نجح كل شيء، وبخلاف ذلك، يتم رفض المكالمة.

وكما هي الحال في throw في إصدار JavaScript القديم العادي، من المعتاد، وليس من الضروري، الرفض باستخدام كائن "خطأ". تتمثل فائدة كائنات الخطأ في أنها تلتقط لتتبُّع تسلسل استدعاء الدوال البرمجية، ما يجعل أدوات تصحيح الأخطاء أكثر فائدة.

إليك كيفية الاستفادة من هذا الوعد:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

تأخذ then() وسيطتين، استدعاء لحالة نجاح، واستدعاء آخر. لحالة الإخفاق. كلاهما اختياري، لذا يمكنك إضافة معاودة اتصال في حالة النجاح أو الفشل فقط.

ظهرت وعود JavaScript في نموذج DOM باسم "المستقبلات" (Futures)، وتم تغيير اسمها إلى "التعهدات"، ثم نقلوا أخيرًا إلى JavaScript. فاستخدامها بلغة JavaScript بدلاً من يعتبر نموذج العناصر في المستند (DOM) أداة رائعة لأنها ستكون متاحة في سياقات JavaScript التي لا تستند إلى متصفّح، مثل Node.js (سؤال آخر حول ما إذا كان يستخدمها في واجهات برمجة التطبيقات الأساسية).

فعلى الرغم من أنها ميزة من ميزات JavaScript، إلا أن DOM لا يخشى استخدامها. ضِمن وفي الواقع، ستعتمد جميع واجهات برمجة تطبيقات DOM الجديدة ذات طرق النجاح/الإخفاق غير المتزامنة على الوعود. هذا يحدث بالفعل مع إدارة الحصص أحداث تحميل الخط، ServiceWorker، أجهزة MIDI على الويب، ساحات المشاركات وغير ذلك

التوافق مع المكتبات الأخرى

ستتعامل واجهة برمجة التطبيقات بلغة JavaScript مع أي شيء يستخدم طريقة then() باعتباره شبيهة بالوعد (أو thenable عند تنهد)، لذا عند استخدام مكتبة التي تُرجع وعود Q، فلا بأس بذلك، فسيسير الأمر بشكل جيد مع وعد استخدام JavaScript.

ومع ذلك، كما ذكرت، تعد أوامر jQuery غير مفيدة بعض الشيء. لحسن الحظ، يمكنك توجيههم إلى الوعود المعتادة، وهذا أمر قيّم في أقرب وقت ممكن:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

هنا، تعرض $.ajax في jQuery متغيرًا مؤجلًا. وبما أنّ هذه الطريقة تتضمّن طريقة then()، بإمكان Promise.resolve() تحويل النص إلى وعد باستخدام JavaScript. ومع ذلك، وتمرر التأجيل أحيانًا عدة وسيطات إلى استدعاءاتها، على سبيل المثال:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

بينما تتعهد JS بتجاهل الكل باستثناء الأول:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

لحسن الحظ أن هذا عادة ما يكون ما تريده، أو على الأقل يمنحك إمكانية الوصول إلى ما تريد. عليك أيضًا الانتباه إلى أن jQuery لا يتبع اصطلاح تمرير كائنات الخطأ إلى حالات الرفض.

تسهيل استخدام الرموز البرمجية غير المتزامنة

حسنًا، لنقم بترميز بعض الأشياء. لنفترض أننا نريد:

  1. بدء دوّارة للإشارة إلى التحميل
  2. استرجِع بعض ملفات JSON الخاصة بقصة معيَّنة للحصول على عنوانها وعناوين URL الخاصة بها لكل فصل.
  3. إضافة عنوان إلى الصفحة
  4. جلب كل فصل
  5. إضافة القصة إلى الصفحة
  6. إيقاف الحلقة الدوّارة

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

بالطبع، لن تستخدم JavaScript لعرض المحتوى. العرض بتنسيق HTML بشكل أسرع ولكن هذا النمط شائع جدًا عند التعامل مع واجهات برمجة التطبيقات: عملية التحميل، ثم القيام بشيء ما عند انتهاء كل شيء.

لنبدأ، لنتعامل مع استرجاع البيانات من الشبكة:

طلب XMLHttp واعد

سيتم تحديث واجهات برمجة التطبيقات القديمة لاستخدام الوعود، إذا كان ذلك ممكنًا بالعكس ومتوافقة. XMLHttpRequest هو المرشّح الرئيسي، ولكن في الوقت نفسه لنكتب دالة بسيطة لتقديم طلب GET:

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

الآن، دعنا نستخدمه:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

يمكننا الآن إجراء طلبات HTTP بدون كتابة XMLHttpRequest يدويًا، وهو أمر رائع، لأن في الوقت الذي لا أرى فيه غطاء الجمل المثير للانزعاج لـ "XMLHttpRequest"، ستكون حياتي أسعد.

السلاسل

ليست then() نهاية القصة، يمكنك ربط then معًا تحويل القيم أو تنفيذ إجراءات إضافية غير متزامنة الواحدة تلو الأخرى.

تحويل القيم

ويمكنك تحويل القيم ببساطة عن طريق عرض القيمة الجديدة:

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

كمثال عملي، لنعد إلى:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

الرد بتنسيق JSON، ولكننا نرسله حاليًا كنص عادي. أر يمكن أن نغير دالة get لاستخدام JSON responseType، ولكن يمكننا أيضًا حلها من خلال الوعود:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

بما أن JSON.parse() تأخذ وسيطة واحدة وتعرض قيمة محوّلة، يمكننا إنشاء اختصار:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

في الواقع، يمكننا إنشاء دالة getJSON() بسهولة تامة:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

ما زالت getJSON() تعرض وعودًا، واحدة تجلب عنوان URL ثم تحلّل الاستجابة بتنسيق JSON.

إضافة الإجراءات غير المتزامنة إلى "قائمة المحتوى التالي"

ويمكنك أيضًا ربط then لتنفيذ الإجراءات غير المتزامنة بالتسلسل.

عند إرجاع عنصر من معاودة الاتصال على then()، سيكون الأمر رائعًا بالنسبة إليك. وإذا عرضت قيمة، سيتم استدعاء then() التالية بهذه القيمة. ومع ذلك، في حال إرجاع منتج يشبه الوعود، تنتظر "then()" التالية، لا يسمى إلا عندما يستقر هذا الوعد (نجاح/فشل). على سبيل المثال:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

هنا نقدم طلبًا غير متزامن إلى story.json، والذي يمنحنا مجموعة من عناوين URL المطلوبة، ثم نطلب أولهما. هذا هو وقت الوعود في التميز حقًا عن أنماط معاودة الاتصال البسيطة.

يمكنك أيضًا إنشاء طريقة مختصرة لعرض الفصول:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

لا يتم تنزيل story.json حتى يتم الاتصال بـ getChapter، ولكن سيتم تنزيل ويُسمى الوقت getChapter أنّنا نعيد استخدام وعود القصة، لذلك story.json مرة واحدة فقط. وعود رائعة!

خطأ أثناء المعالجة

كما رأينا سابقًا، تستخدم then() وسيطتين، واحدة للنجاح، والأخرى عند الإخفاق (أو الوفاء والرفض بالوعود:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

يمكنك أيضًا استخدام catch():

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

ما من مزايا خاصة في catch()، يمكن استخدامها فقط عن طريق السكر then(undefined, func)، ولكن يمكن قراءته بسهولة أكبر. لاحظ أن الكودين الأمثلة أعلاه لا تعمل بالطريقة نفسها، فالأخيرة يعادل:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

الفرق دقيق، لكنه مفيد للغاية. تخطّي الوعود برفض الدفعات إعادة التوجيه إلى then() التالية مع معاودة اتصال مرفوضة (أو catch()، لأن مماثلة). في حال استخدام "then(func1, func2)" أو "func1" أو "func2"، سيكون وليس كليهما. ولكن مع then(func1).catch(func2)، سيصبح كلاهما سيتم الاستدعاء إذا تم رفض func1، لأنها خطوات منفصلة في السلسلة. استقِل: ما يلي:

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

ويشبه التدفق أعلاه إلى حد كبير تجربة/الالتقاط العادي في JavaScript، وهي الأخطاء التي تحدث ضمن "محاولة" الانتقال فورًا إلى جزء catch(). إليك أعلاه كمخطط انسيابي (لأنني أحب المخططات الانسيابية):

اتبع الأسطر الزرقاء للوعود التي تفي بها، أو اتبع الخطوط الحمراء للوعود التي تفي بها الرفض.

استثناءات و وعود JavaScript

تحدث عمليات الرفض عندما يتم رفض وعود بشكلٍ صريح ولكن أيضًا بشكل ضمني إذا تم طرح خطأ في استدعاء الدالة الإنشائية:

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

وهذا يعني أنه من المفيد القيام بجميع الأعمال المتعلقة بالوعود داخل دالة إنشاء وعود، بحيث يتم اكتشاف الأخطاء تلقائيًا تصبح عمليات رفض.

وينطبق ذلك أيضًا على الأخطاء التي تحدث في عمليات استدعاء then().

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

حدث خطأ أثناء المعالجة

من خلال قصتنا وفصولنا، يمكننا استخدام أداة البحث لعرض خطأ للمستخدم:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

إذا تعذّر جلب story.chapterUrls[0] (مثل http 500 أو كان المستخدم بلا اتصال بالإنترنت)، سيتخطى جميع استدعاءات النجاح التالية، والتي تتضمن تلك في getJSON() الذي يحاول تحليل الرد بتنسيق JSON، ويتخطّى أيضًا رد الاتصال الذي يضيف chapter1.html إلى الصفحة. وبدلاً من ذلك، ينتقل إلى الصيد معاودة الاتصال. ونتيجةً لذلك، "تعذّر عرض الفصل". ستتم إضافته إلى الصفحة إذا فشل أي من الإجراءات السابقة.

مثلما يحدث مع المحاولة/الالتقاط في JavaScript، يتم اكتشاف الخطأ ويتم إرسال الرمز اللاحق حتى تكون الدائرة مخفية دائمًا، وهذا ما نريده. تشير رسالة الأشكال البيانية أعلاه نسخة غير متزامنة لا تؤدي إلى الحظر:

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

يمكنك catch() فقط لأغراض التسجيل، بدون استرداد الحساب. عن الخطأ. للقيام بذلك، ما عليك سوى إعادة إظهار الخطأ. يمكننا القيام بذلك في طريقة getJSON():

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

ولذا تمكنا من جلب فصل واحد، لكننا نريدها جميعًا. لنجعل التي تحدث.

التوازي والتسلسل: الاستفادة إلى أقصى حد من كليهما

إنّ التفكير في عدم المزامنة ليس بالأمر السهل. إذا كنت تكافح للخروج من الهدف، وحاول كتابة الرمز كما لو كان متزامنًا. في هذه الحالة:

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

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

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

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

هذا مناسب! إلا أنه تتم مزامنتها ويقفل المتصفح أثناء تنزيل الأشياء. إلى لجعل هذا العمل غير متزامن، نستخدم then() لتنفيذ الإجراءات الواحد تلو الآخر.

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

  // TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

ولكن كيف يمكننا تكرار عناوين url الخاصة بالفصول وجلبها بالترتيب؟ هذا النمط لا تعمل:

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

العلامة forEach غير متزامنة، لذا ستظهر فصولنا بأي ترتيب الذي نزّلوه، وهو في الأساس الطريقة التي كُتِب بها فريق Pulp افتراضي. لا يمثل ذلك لنصلح الأمر من باب الخيال.

إنشاء تسلسل

نريد تحويل مصفوفة chapterUrls إلى سلسلة من الوعود. ويمكننا إجراء ذلك باستخدام then():

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

هذه هي المرة الأولى التي نشاهد فيها Promise.resolve()، الذي أنشأ التي تحلى بأي قيمة تقدمها له. إذا اجتزت ذلك مثيل لـ Promise، فسيتم إرجاعه ببساطة (ملاحظة: هذا التغيير إلى المواصفات التي لا تتبعها بعض عمليات التنفيذ). إذا كنت الانتقال إلى شيء يشبه وعودًا (يتناول طريقة then())، ينشئ Promise حقيقي ينفّذ/يرفض بالطريقة نفسها في حال اجتياز الاختبار بأي قيمة أخرى، مثال: Promise.resolve('Hello')، ينشئ تقديم وعد يفي بهذه القيمة. إذا استدعيته بدون قيمة، كما هو موضح أعلاه، تفي بـ "غير محدد".

هناك أيضًا Promise.reject(val)، الذي يقطع وعدًا يرفضه القيمة التي تقدمها لها (أو غير محددة).

يمكننا ترتيب التعليمة البرمجية أعلاه باستخدام array.reduce:

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

يفعل ذلك مثل المثال السابق، لكن لا يحتاج إلى الفصل "تسلسل" المتغير. يتم استدعاء تقليل استدعاء الدالة لكل عنصر في الصفيفة. "تسلسل" هو Promise.resolve() في المرة الأولى، ولكن في بقية استدعاء "التسلسل" هو كل ما عدناه من المكالمة السابقة. array.reduce وهو مفيد حقًا لتحويل صفيفة إلى قيمة واحدة، وهو ما يمثل في هذه الحالة هو وعد.

لنضع كل شيء معًا:

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

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

وهذا كل ما في الأمر: نسخة غير متزامنة تمامًا من إصدار المزامنة. لكن يمكننا القيام أَفْضَلْ. في الوقت الحالي، يتم تنزيل صفحتنا على النحو التالي:

تتميز المتصفحات بسهولة تنزيل العديد من العناصر في وقت واحد، لذلك سنفقد عن طريق تنزيل الفصول واحدًا تلو الآخر. ما نريد القيام به هو وتنزيلها جميعًا في الوقت نفسه، ثم معالجتها عند وصولها جميعًا. لحسن الحظ، هناك واجهة برمجة تطبيقات لهذا:

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

تتشارك Promise.all مجموعة من الوعود وتنطوي على وعد يلبّيها عند اكتمالها جميعًا بنجاح. تحصل على مصفوفة من النتائج (بغض النظر عن التي وعدتَ بها) بنفس ترتيب الوعود التي مررت بها.

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

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

اعتمادًا على نوع الاتصال، قد يكون التحميل أسرع من التحميل الواحد تلو الآخر بالثواني وهو أقل من التعليمات البرمجية من محاولتنا الأولى. يمكن تنزيل الفصول في أي بالترتيب، لكنها تظهر على الشاشة بالترتيب الصحيح.

ومع ذلك، لا يزال بإمكاننا تحسين الأداء الملحوظ. عند وصول الفصل الأول، يجب إضافته إلى الصفحة. وهذا يتيح للمستخدم بدء القراءة قبل بقية وصلت الفصول. عندما يصل الفصل الثالث، لن نضيفه إلى لأن المستخدم قد لا يدرك أن الفصل الثاني مفقود. عندما الفصل الثاني؟ فيمكننا إضافة الفصلين الثاني والثالث، إلخ.

للقيام بذلك، نجلب JSON لجميع فصولنا في نفس الوقت، ثم ننشئ تسلسل لإضافتها إلى المستند:

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

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence
      .then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

وها نحن ذا، أفضل ما في كليهما! يستغرق نفس القدر من الوقت لتقديم كل المحتوى، ولكن يحصل المستخدم على أول جزء من المحتوى بشكل أسرع.

في هذا المثال التافه، تصل جميع الفصول في نفس الوقت تقريبًا، ولكن فإن فائدة عرض واحد في كل مرة تصبح مبالغًا فيها بقيمة أكبر فصولها.

يؤدي تنفيذ ما سبق باستخدام استدعاءات نمط Node.js أو الأحداث حوالي ومضاعفة التعليمات البرمجية، ولكن الأهم من ذلك ليس من السهل اتباعه. ومع ذلك، ليست نهاية المطاف، عند دمجها مع ميزات ES6 الأخرى تصبح أكثر سهولة.

جولة إضافية: توسيع نطاق الإمكانات

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

نشكر "آن فان كسترين" و"دومينيك دينيكولا" و"توم آشوورث" و"ريمي شارب"، "أدي عثمان" و"آرثر إيفانز" و"يوتاكا هيرانو" الذين صححوا هذا النص وقاموا التصحيحات/التوصيات.

بالإضافة إلى ذلك، بفضل ماتياس بينينز تحديث أجزاء مختلفة المقالة.