تعمل التعهدات على تبسيط العمليات الحسابية المؤجلة وغير المتزامنة. الوعد يمثل عملية لم تكتمل بعد.
بالنسبة للمطوّرين، ندعوك للاستعداد للحظة المحورية في تاريخ تطوير الويب.
[يبدأ الطبول]
وصول التعهدات إلى JavaScript
[تنفجر الألعاب النارية، وأوراق الأمطار اللامعة التي تتلألأ من الأعلى، والحشود تنتشر في أجواء جامحة]
في هذه المرحلة، أنت تندرج ضمن إحدى الفئات التالية:
- يشجع الناس من حولك، ولكنك لست متأكدًا من سبب كل هذا الهمة. ربما لا تكون متأكدًا من ماهية "الوعد". تهز كتفيك، لكن وزن الورق اللامع ثقيل على كتفيك. إذا كان الأمر كذلك، فلا تقلق بشأن ذلك، فقد استغرق الأمر وقتًا طويلاً لمعرفة سبب الاهتمام بهذه الأشياء. من المحتمل أنك تريد أن تبدأ من البداية.
- لكمة في الهواء! أليس كذلك؟ سبق أن استخدمت عناصر "الوعد" هذه من قبل، ولكن يزعجك أن جميع عمليات التنفيذ لها واجهة برمجة تطبيقات مختلفة قليلاً. ما هي واجهة برمجة التطبيقات لإصدار JavaScript الرسمي؟ ننصحك بالبدء بالمصطلحات.
- لقد علمت بشأن هذا الأمر بالفعل وأنت تستهين بأولئك الذين يقفزون صعودًا ونزولاً كما لو كان ذلك أخبارًا لهم. خصص بعض الوقت للاستمتاع بتميزك، ثم توجه مباشرة إلى مرجع واجهة برمجة التطبيقات.
دعم المتصفح وpolyfill
للجمع بين المتصفحات التي تفتقر إلى تنفيذ وعود كاملة بما يصل إلى الامتثال للمواصفات، أو لإضافة وعود إلى المتصفحات الأخرى وNode.js، راجع polyfill (2k gzipped).
ما هو كل هذا الضجّة؟
لغة JavaScript عبارة عن سلسلة تعليمات واحدة، ما يعني أنّه لا يمكن تشغيل مكوّنَين من النص البرمجي في الوقت نفسه، إذ يجب تشغيلهما واحدًا تلو الآخر. في المتصفحات، تشارك 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 مرة، وكتب رسالة قلقًا إلى والدي. على الرغم من ذلك، ما زلت أختلط الكثير من المصطلحات، ولكن إليك الأساسيات:
يمكن أن يكون الوعد:
- fulfill: تم تنفيذ الإجراء المتعلق بالوعد بنجاح.
- rejected (تم رفض الإجراء) - تعذّر تنفيذ الإجراء المرتبط بالوعد.
- Pending (في انتظار المراجعة) - لم يتم توصيلها أو رفضها بعد
- settled - تم تنفيذ المنتج أو رفضه.
تستخدم المواصفات
أيضًا المصطلح thenable لوصف كائن يشبه الوعد،
بأنه يستخدم طريقة then
. يذكرني هذا المصطلح بالمدير السابق لكرة القدم
في إنجلترا تيري فينالز،
لذا سأستخدمها بأقل قدر ممكن.
تظهر الوعود بتنسيق JavaScript
وقد ظهرت الوعود منذ فترة في شكل مكتبات، مثل:
إنّ ما ورد أعلاه ووعد JavaScript يتّبعان سلوكًا موحدًا وشائعًا يُسمى Promises/A+. وإذا كنت من مستخدمي jQuery، سيكون لديك ما يشبه المؤجلات. مع ذلك، لا تتوافق قيم التأجيل مع Promise/A+ ، ما يجعلها مختلفة بدرجة كبيرة وأقل فائدة، لذا يجب توخّي الحذر. فأداة jQuery تحتوي أيضًا على نوع Promise، ولكن هذه ليست سوى مجموعة فرعية من الطلبات المؤجلة والتي تتضمّن المشاكل نفسها.
على الرغم من أن عمليات تنفيذ الوعد تتبع سلوكًا موحدًا، فإن واجهات برمجة التطبيقات العامة تختلف عن بعضها البعض. وتتشابه وعود 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) باسم "العقود المستقبلية"، ثم تمت إعادة تسميتها إلى "التعهدات"، ثم في النهاية نقلها إلى JavaScript. ومن الرائع إدراجها في JavaScript بدلاً من نموذج كائن المستند (DOM)، لأنّها ستكون متاحة في سياقات JavaScript لا تستند إلى متصفّح، مثل Node.js (وما إذا كانوا يستخدمونها في واجهات برمجة التطبيقات الأساسية).
وعلى الرغم من كونها إحدى ميزات JavaScript، إلا أن DOM لا يخشى استخدامها. في الواقع، جميع واجهات برمجة تطبيقات DOM الجديدة ذات طرق النجاح/الفشل غير المتزامنة ستستخدم الوعود. ويحدث ذلك حاليًا في إدارة الحصص وأحداث تحميل الخطوط وServiceWorker وواجهة برمجة التطبيقات على الويب وساحات المشاركات وغيرها.
التوافق مع المكتبات الأخرى
ستتعامل واجهة برمجة التطبيقات التي تعدك في 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) {
// ...
})
بينما تعد لغة JavaScript بتجاهل الكل باستثناء الأول:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
لحسن الحظ أن هذا هو ما تريده عادةً، أو على الأقل يمنحك الوصول إلى ما تريد. انتبه أيضًا إلى أن jQuery لا تتبع اصطلاح تمرير كائنات الخطأ إلى حالات الرفض.
تم تسهيل الرموز غير المتزامنة المعقدة
حسنًا، لنبرمج بعض الأشياء. لنفترض أننا نريد:
- تشغيل دائرة دوارة للإشارة إلى التحميل
- يمكنك جلب ملف JSON لقصة، وبذلك نحصل على العنوان وعناوين URL لكل فصل.
- إضافة عنوان إلى الصفحة
- جلب كل فصل
- إضافة القصة إلى الصفحة
- إيقاف الدوران
... ولكن أخبر المستخدم أيضًا إذا حدث خطأ ما أثناء العملية. سنحتاج إلى إيقاف المؤشر الدوار عند هذه النقطة أيضًا، وإلا سيستمر في الدوران، يصاب بالدوار، ويحدث عطلاً في واجهة مستخدم أخرى.
بالطبع، لن تستخدم JavaScript لعرض قصة، إذ إنّ استخدام HTML يكون أسرع، إلا أنّ هذا النمط شائع جدًا عند التعامل مع واجهات برمجة التطبيقات: عمليات جلب بيانات متعددة، ثم تنفيذ إجراءات معيّنة بعد تنفيذ كل ذلك.
للبدء، دعنا نتعامل مع جلب البيانات من الشبكة:
تقديم طلب XMLHttpRequest بشكل صريح
سيتم تحديث واجهات برمجة التطبيقات القديمة لاستخدام الوعود، إذا كان ذلك ممكنًا بطريقة متوافقة مع الأنظمة القديمة. 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);
})
معالجة الأخطاء عمليًا
من خلال قصتنا والفصول، يمكننا استخدام drop لإظهار الخطأ للمستخدم:
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 تعرَّف على كل روائعنا. هذه ليست Pulp Fantasy،
لذا دعنا نصلحها.
إنشاء تسلسل
نريد تحويل مصفوفة 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())
وهذه الطريقة هي نفسها المستخدمة في المثال السابق، ولكن لا تحتاج إلى متغير "tracker" المنفصل. يتم استدعاء استدعاء التقليل الخاص بنا لكل عنصر في المصفوفة.
تكون قيمة "تسلسل" 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 الأخرى تكون أسهل.
جولة إضافية: إمكانات موسّعة
منذ أن كتبت هذه المقالة في الأصل، توسعت القدرة على استخدام "التعهدات" إلى حد كبير. منذ إصدار 55 من Chrome، سمحت الدوال غير المتزامنة بكتابة الرموز البرمجية المستندة إلى الوعد كما لو كانت متزامنة، ولكن بدون حظر سلسلة التعليمات الرئيسية. يمكنك قراءة المزيد عن هذا الموضوع في my async functions article. هناك دعم واسع النطاق لكل من الوعود والوظائف غير المتزامنة في المتصفحات الرئيسية. يمكنك العثور على التفاصيل في مرجع Promise والدالة غير المتزامنة في MDN.
شكرًا جزيلاً لـ "آن فان فان كيسترين" و"دومينيك دينيكولا" و"توم أشوورث" و"ريمي شارب" و"أدي عثمان" و"آرثر إيفانز" و"يوتاكا هيرانو"، الذين صححوا هذا الأمر وأجروا تصحيحات/اقتراحات.
ونودّ أيضًا أن نتوجّه بالشكر إلى ماثياس بيننز لتعديل أجزاء مختلفة من المقالة.