يمثّل كائن Promise الإتمام النهائي (أو الفشل) لعملية غير متزامنة وقيمتها الناتجة.
هذا موضوع صعب على كثيرين، خصوصًا إن كنت تعرف البرمجة بلغة متزامنة بالكامل. إذا شعرت أن الأمر يفوقك، أو أردت معرفة المزيد عن التزامن والتوازي، فـشاهد (عبر go.dev) أو شاهد مباشرة عبر vimeo واقرأ الشرائح للمحاضرة الرائعة "Concurrency is not parallelism".
للوعد Promise ثلاث حالات:
عند إنشائه، يكون الوعد معلّقًا. وفي وقت لاحق قد يُنجَز أو يُرفَض. وبمجرد أن يُنجَز الوعد أو يُرفَض مرة واحدة، لا يمكن أن يُنجَز أو يُرفَض مرة أخرى، ولا يمكن أن تتغيّر حالته.
وبعبارة أخرى:
يمكن إنجاز الوعد بطرق متعددة:
// Creates a promise that is immediately resolved
Promise.resolve(value);
// Creates a promise that is immediately resolved
new Promise((resolve) => {
resolve(value);
});
// Chaining a promise leads to a resolved promise
somePromise.then(() => {
// ...
return value;
});
في الأمثلة أعلاه يمكن أن يكون value أي شيء، بما في ذلك خطأ أو undefined أو null أو وعد آخر.
وعادةً تريد الإنجاز بقيمة ليست خطأ.
يمكن رفض الوعد بطرق متعددة:
// Creates a promise that is immediately rejected
Promise.reject(reason)
// Creates a promise that is immediately rejected
new Promise((_, reject) {
reject(reason)
})
// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
// ...
throw reason
})
في الأمثلة أعلاه يمكن أن يكون reason أي شيء، بما في ذلك خطأ أو undefined أو null.
وعادةً تريد الرفض بسبب خطأ.
يمكن متابعة الوعد بإجراء لاحق بمجرد أن يُنجَز أو يُرفَض.
promise.then() بمجرد أن يُنجَز promise
promise.catch() بمجرد أن يُرفَض promise
promise.finally() بمجرد أن يُنجَز promise أو يُرفَضكل وعد قابل للربط بـ then.
وهذا يعني وجود دالة then متاحة تُنفَّذ بمجرد إنجاز الوعد الأصلي.
عند استخدام promise.then(onResolved)، تستقبل دالة رد النداء onResolved القيمة التي أُنجز بها الوعد الأصلي.
وسيعيد هذا دائمًا وعدًا جديدًا "مترابطًا".
إرجاع value من then يُنجز الوعد "المترابط".
ورميّ reason في then يرفض الوعد "المترابط".
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
});
const promise2 = promise1.then(function (value) {
console.log(value);
// expected output: "Success!"
return true;
});
سيسجّل هذا "Success!" بعد نحو 1000 مللي ثانية.
وستكون حالة promise1 وقيمته resolved و"Success!".
وستكون حالة promise2 وقيمته resolved وtrue.
وهناك وسيط ثانٍ متاح يُنفَّذ عندما يُرفَض الوعد الأصلي.
عند استخدام promise.then(onResolved, onRejected)، تستقبل دالة رد النداء onResolved القيمة التي أُنجز بها الوعد الأصلي، أو تستقبل دالة رد النداء onRejected سبب رفض الوعد.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return true;
}
function shout(reason) {
console.error(reason.toUpperCase());
return false;
}
const promise2 = promise1.then(log, shout);
"Success!" بعد نحو 1000 مللي ثانية.
promise1 وقيمته resolved و"Success!".promise2 وقيمته resolved وtrue."NOPE!" فورًا.
promise1 وقيمته rejected وNope!.promise2 وقيمته resolved وfalse.من المهم أن تفهم أنه بسبب قواعد دورة الحياة، عندما rejects، فإن resolve الذي يأتي بعد نحو 1000 مللي ثانية يُتجاهَل بصمت، لأن الحالة الداخلية لا يمكن أن تتغيّر بمجرد أن يُرفَض الوعد أو يُنجَز.
ومن المهم أن تفهم أن إرجاع قيمة من وعد يُنجزه، ورميّ قيمة يرفضه.
عندما يُنجَز promise1 ويكون هناك onResolved مترابط: then(onResolved)، فإن ذلك التابع وعد جديد يمكن أن يُنجَز أو يُرفَض.
وعندما يُرفَض promise1 لكن يكون هناك onRejected مترابط: then(, onRejected)، فإن ذلك التابع وعد جديد يمكن أن يُنجَز أو يُرفَض.
أحيانًا تريد التقاط الأخطاء والمتابعة فقط عندما rejects الوعد الأصلي.
عند استخدام promise.catch(onCatch)، تستقبل دالة رد النداء onCatch سبب رفض الوعد الأصلي.
وسيعيد هذا دائمًا وعدًا جديدًا "مترابطًا".
إرجاع value من catch يُنجز الوعد "المترابط".
ورميّ reason في catch يرفض الوعد "المترابط".
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return 'done';
}
function recover(reason) {
console.error(reason.toUpperCase());
return 42;
}
const promise2 = promise1.catch(recover).then(log);
في نحو 1/2 من الحالات، سيسجّل هذا "Success!" بعد نحو 1000 مللي ثانية.
وفي النصف الآخر من الحالات، سيسجّل هذا 42 فورًا.
promise1، يُتخطّى catch ويصل التنفيذ إلى then ويسجّل القيمة.
promise1 وقيمته resolved و"Success!".promise2 وقيمته resolved و"done";promise1، يُنفَّذ catch، الذي يُرجع قيمة، وبذلك تصبح السلسلة الآن resolved، ويصل التنفيذ إلى then ويسجّل القيمة.
promise1 وقيمته rejected و"Nope!".promise2 وقيمته resolved و"done";أحيانًا تريد تنفيذ كود بعد أن يستقر الوعد، بغض النظر عما إذا كان يُنجَز أو يُرفَض.
عند استخدام promise.finally(onSettled)، لا تستقبل دالة رد النداء onSettled شيئًا.
وسيعيد هذا دائمًا وعدًا جديدًا "مترابطًا".
إرجاع value من finally ينسخ الحالة والقيمة من الوعد الأصلي مع تجاهل value.
ورميّ reason في finally يرفض الوعد "المترابط"، ويلغي أي حالة وقيمة أو سبب من الوعد الأصلي.
عدة طرق معًا:
const myPromise = new Promise(function (resolve, reject) {
const sampleData = [2, 4, 6, 8];
const randomNumber = Math.round(Math.random() * 5);
if (sampleData[randomNumber]) {
resolve(sampleData[randomNumber]);
} else {
reject('Sampling did not result in a sample');
}
});
const finalPromise = myPromise
.then(function (sampled) {
// If the random number was 0, 1, 2, or 3, this will be
// reached and the number 2, 4, 6, or 8 will be logged.
console.log(`Sampled data: ${sampled}`);
return 'yay';
})
.catch(function (reason) {
// If the random number was 4 or 5, this will be reached and
// reason will be "An error occurred". The entire chain will
// then reject with an Error with the reason as message.
throw new Error(reason);
})
.finally(function () {
// This will always log after either the sampled data is
// logged or the error is raised.
console.log('Promise completed');
});
randomNumber هو 0-3:
myPromise بالقيمة 2, 4, 6, or 8
finalPromise بالقيمة 'yay'
Sampled data: ...Promise completedrandomNumber هو 4-5:
myPromise بالسبب 'Sampling did not result in a sample'
finalPromise بالسبب Error('Sampling did not result in a sample')
Promise completed"uncaught rejected promise: Error('Sampling did not result in a sample')"
كما هو موضح أعلاه، يعمل reject مع سلسلة نصية، ويمكن للوعد أيضًا أن يُرفَض بكائن Error.
إذا كان ربط الوعود أو الاستخدام العام غير واضح، فإن الدرس التعليمي على MDN مصدر جيد للاطلاع.
في هذا التمرين، ستوفّر TranslationService يقدّم خدمات ترجمة أساسية للأعضاء المجانيين، وترجمة متقدمة للأعضاء المميزين مع ضمانات الجودة.
API
لقد وجدت API للترجمة في الفضاء الخارجي يلبّي أي request ترجمة في وقت معقول.
وتريد أن تستفيد من ذلك.
مترجمو الفضاء متقلبون للغاية ويكرهون التكرار، لذا يوفّرون أيضًا أقمار تخزين API يمكنك من خلالها fetch الترجمات السابقة دون إزعاجهم.
جلب ترجمة
تجلب api.fetch(text) ترجمةً للنص text من تخزين API وتُرجع promise يوفّر قيمتين:
translation: الترجمة الفعليةquality: الجودة معبَّرًا عنها بعددإذا لم يُعثر على ترجمة في تخزين API، فإن API يطرح خطأ NotAvailable.
ويمكن إضافة الترجمات باستخدام الطريقة api.request.
وإذا كان 'text' غير قابل للترجمة، فإن API يطرح خطأ Untranslatable.
api.fetch('jIyaj');
// => Promise({ resolved: 'I understand' })
طلب ترجمة
بعض الترجمات موجودة بالتأكيد، لكنها لم تُضف بعد إلى تخزين API. وهذا هو الفرق بين NotAvailable (غير موجود في التخزين، لكن يمكن طلبه) وUntranslatable (لا يمكن ترجمته).
يطلب api.request(text, callback) تنفيذ ترجمة للنص text وإضافتها إلى تخزين API.
وعند الانتهاء تُستدعى دالة callback.
undefined إلى callback: وهذا يعني أن الترجمة نجحت ويمكن الوصول إليها باستخدام الطريقة api.fetch.error إلى callback: وهذا يعني أن شيئًا ما سار على نحو خاطئ.
إن API الفضاء غير مستقر، ما يعني أنه يفشل كثيرًا.
وإذا حدث ذلك، فلا بأس من استدعاء api.request مرة أخرى.api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
// because it was successful.
⚠ تحذير! ⚠
يعمل API سحره بنقل المترجمين المختلفين آنيًا عندما يصل request.
وهذا إجراء مكلف للغاية، لذلك لا ينبغي استدعاؤه عندما تكون الترجمة متاحة.
ولسوء الحظ، لا يقرأ الجميع الدليل، لذا هناك نظام معمول به لطرد المخالفين.
إذا استُدعي api.request لنص متاح، فإن API يطرح AbusiveClientError لهذا الاستدعاء، ولكل استدعاء بعده.
تأكد من أنك لا تطلب ترجمة أبدًا إذا كان شيء ما قد تُرجم بالفعل.
لا تقدّم الخدمة المجانية إلا الترجمات الموجودة حاليًا في تخزين API.
نفّذ طريقة free(text) توفّر للأعضاء المجانيين ترجمة موجودة بالفعل في تخزين API.
تجاهل الجودة ومرّر أي أخطاء يطرحها API.
api.fetch (تُرجع api.fetch قيمة promise)service.free('jIyaj');
// => Promise<...> resolves "I understand."
service.free("jIyajbe'");
// => Promise<...> rejects Error("Not yet translated")
نفّذ طريقة batch([text, text, ...]) للأعضاء المجانيين تترجم مصفوفة من النصوص باستخدام الخدمة المجانية، وتُرجع كل الترجمات أو خطأً واحدًا.
BatchIsEmpty إذا لم يُعطَ أي نصservice.batch(['jIyaj', "majQa'"]);
// => Promise<...> resolves ["I understand.", "Well done!"]
service.batch(['jIyaj', "jIyajbe'"]);
// => Promise<...> rejects new Error("Not yet translated")
service.batch([]);
// => Promise<...> rejects BatchIsEmpty()
نفّذ طريقة للمستخدمين المميزين باسم request(text) تطلب أن تُضاف ترجمة إلى تخزين API.
وينبغي أن يعيد الطلب المحاولة تلقائيًا إذا حدث فشل.
وينبغي ألا يجري أكثر من 3 استدعاءات للطلب نفسه (لا تغضب مترجمي الفضاء!!!).
api.request خطأً، فحلّ بـundefined
api.request خطأً، فأعد المحاولة مرتين على الأكثرservice.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API
نفّذ طريقة للمستخدمين المميزين premium(text, quality) لجلب ترجمة.
إذا كانت الترجمة NotAvailable، فاطلب الترجمة واجلبها بعد إضافتها إلى تخزين API.
وينبغي ألا تُرجع الطريقة الترجمة إلا إذا بلغت حدًا معينًا من quality.
api.fetch، فافحص الجودة قبل الحلapi.fetch، فـ_اطلب_ الترجمة بدلًا من ذلكapi.request، فمرّر الخطأservice.premium("jIyajbe'", 100);
// => Promise<...> resolves "I don't understand."
service.premium("'arlogh Qoylu'pu'?", 100);
// => Promise<...> rejects QualityThresholdNotMet()
service.premium("'arlogh Qoylu'pu'?", 40);
// => Promise<...> resolves "What time is it?"
ملحوظة
الترجمة الصحيحة لـ'arlogh Qoylu'pu'? هي How many times has it been heard?.
سجّل في Exercism لتتعلّم وتتقن JavaScript عبر 37 مفهومًا159 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.