اذهب إلى المحتوى

الوعود Promise في جافاسكربت


صفا الفليج

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

وكي تُريح بالك تعدهم بإرسالها إليهم ما إن تُنشر. فتُعطي مُعجبيك قائمة يملؤون فيها عناوين بريدهم. ومتى ما نشرت الأغنية يستلمها كلّ من في تلك القائمة. ولو حصل مكروه (لا سمح الله) مثل أن شبّت النار والتهمت الأستديو ولم تقدر على نشر الأغنية - لو حصل ذلك فسيعلمون به أيضًا.

وعاش الجميع بسعادة وهناء: أنت إذ لا يُزعجك الجميع بالتهديدات والتوعّدات، ومُعجبيك إذ لن تفوتهم أيّة رائعة من روائعك الفنية.

إليك ما يشبه الأمور التي نفعلها في الحياة الواقعية - في الحياة البرمجية:

  1. ”شيفرة مُنتِجة“ تُنفّذ شيئًا وتأخذ الوقت. مثل الشيفرات التي تُحمّل البيانات عبر الشبكة. هذا أنت، ”المغنّي“.
  2. ”شيفرة مُستهلِكة“ تطلب ناتج ”الشيفرة المُنتِجة“ ما إن يجهز. وهناك عديد من الدوال تحتاج إلى هذا الناتج. هذه ”مُعجبوك“.
  3. الوعد (Promise) هو كائن فريد في جافاسكربت يربط بين ”الشيفرة المُنتِجة“ و”الشيفرة المُستهلِكة“. في الحياة العملية، الوعد هو ”قائمة الاشتراك“. يمكن أن تأخذ ”الشيفرة المُنتِجة“ ما تلزم من وقت لتقدّم لنا النتيجة التي وعدتنا بها، وسيُجهّزها لنا ”الوعد“ لأيّة شيفرة طلبتها متى جهزت.

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

هذه صياغة الباني لكائنات الوعد:

let promise = new Promise(function(resolve, reject) {
  // ‫المُنفِّذ (الشيفرة المُنتجة، مثل ”المغنّي“)
});

تُدعى الدالة الممرّرة إلى new Promise ”بالمُنفِّذ“. متى صُنع الوعد new Promise عملت الدالة تلقائيًا. يحتوي هذا المُنفِّذ الشيفرة المُنتجِة، ويمكن أن تقدّم لنا في النهاية ناتجًا. في مثالنا أعلاه، فالمُنفِّذ هذا هو ”المغنّي“.

تقدّم جافاسكربت الوسيطين resolve و reject وهما ردود نداء. كما ولا نضع الشيفرة التي نريد تنفيذها إلا داخل المُنفِّذ.

لا يهمّنا متى سيعرف المُنفِّذ الناتجَ (آجلًا كان ذلك أم عاجلًا)، بل أنّ عليه نداء واحدًا من ردود النداء هذه:

  • resolve(value)‎: لو اكتملت المهمّة بنجاح. القيمة تسجّل في value.
  • reject(error)‎: لو حدث خطأ. error هو كائن الخطأ.

إذًا نُلخّص: يعمل المُنفِّذ تلقائيًا وعليه مهمّة استدعاء resolve أو reject.

لكائن الوعد promise الذي أعاده الباني new Promise خاصيتين داخليتين:

  • الحالة state: تبدأ بالقيمة "pending" وبعدها تنتقل إلى "fulfilled" متى استُدعت resolve، أو إلى "rejected" متى استُدعت reject.
  • الناتج result: يبدأ أولًا غير معرّف undefined، وبعدها يتغيّر إلى value متى استُدعت resolve(value)‎ أو يتغيّر إلى error متى استُدعت reject(error)‎.

وفي النهاية ينقل المُنفِّذ الوعدَ promise ليصير بإحدى الحالات الآتية:

promise-resolve-reject.png

سنرى لاحقًا كيف سيشترك ”مُعجبونا“ بهذه التغييرات.

إليك مثالًا عن بانيًا للوعود ودالة مُنفِّذ بسيطة فيها ”شيفرة مُنتجِة“ تأخذ بعض الوقت (باستعمال setTimeout?

let promise = new Promise(function(resolve, reject) {
  // تُنفّ الدالة مباشرةً ما إن يُصنع الوعد

  // ‫وبعد ثانية واحدة نبعث بإشارة بأنّ المهمة انتهت والنتيجة هي ”تمت“ (done)
  setTimeout(() => resolve("done"), 1000);
});

بتشغيل الشيفرة أعلاه، نرى أمرين اثنين:

  1. يُستدعى المُنفِّذ تلقائيًا ومباشرةً (عند استعمال new Promise).
  2. يستلم المُنفِّذ وسيطين: دالة الحلّ resolve ودالة الرفض reject، وهي دوال معرّفة مسبقًا في محرّك جافاسكربت، ولا داعٍ بأن نصنعها نحن، بل استدعاء واحدة ما إن تجهز النتيجة.

بعد سنة من عملية ”المعالجة“ يستدعي المُنفِّذ الدالةَ resolve("done")‎ لتُنتج الناتج. هكذا تتغيّر حالة كائن promise:

promise-resolve-1.png

كان هذا مثالًا عن مهمّة اكتملت بنجاح، أو ”وعد تحقّق“.

والآن سنرى مثالًا عن مُنفِّذ يرفض الوعد مُعيدًا خطأً:

let promise = new Promise(function(resolve, reject) {
  // بعد ثانية واحدة نبعث بإشارة بأنّ المهمة انتهت ونُعيد خطأً
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

باستدعاء reject(...)‎ ننقل حالة كائن الوعد إلى حالة الرفض "rejected":

promise-reject-1.png

ملخّص القول هو أنّ على المُنفِّذ تنفيذ المهمة (أي ما يأخذ بعض الوقت ليكتمل) وثمّ يستدعي واحدةً من الدالتين resolve أو reject لتغيير حالة كائن الوعد المرتبط بالمُنفِّذ.

يُسمّى الوعد الذي تحقّق أو نُكث الوعد المنُجز، على العكس من الوعد المعلّق.

ملاحظة: إما أن تظهر نتيجة واحدة أو خطأ، يجب على المنفّذ أن يستدعي إما resolve أو reject. أي تغيير في الحالة يعدّ تغييرًا نهائيًا.

وسيُتجاهل جميع الاستدعاءات اللاحقة سواءً أكانت resolve أو reject:

let promise = new Promise(function(resolve, reject) {

  resolve("done");

  reject(new Error("…")); // ستتجاهل
  setTimeout(() => resolve("…")); // ستتجاهل
});

الفكرة هنا أن خرج عمل المنفذّ سيعرض إما نتيجة معينة أو خطأ.

وتتوقع التعليمتين resolve/reject وسيطًا واحدًا مُررًا (أو بدون وسطاء نهائيًا) وأي وسطاء إضافية ستُتجاهل.

ملاحظة: الرفض مع كائن Error في حال حدوث خطأ ما، يجب على المنفذّ أن يستدعي تعليمة reject. ويمكن تمرير أي نوع من الوسطاء (تمامًا مثل: resolve). ولكن يوصى باستخدام كائنات Error (أو أي كائنات ترث من Error). وقريبًا سنعرف بوضوح سبب ذلك.

ملاحظة: استدعاء resolve/reject الفوري عمليًا عادة ينجز المنفذّ عمله بشكل متزامن ويستدعي resolve/reject بعد مرور بعض الوقت، ولكن الأمر ليس إلزاميًا، يمكننا استدعاء resolve أو reject فورًا، هكذا:

let promise = new Promise(function(resolve, reject) {
  // يمكننا القيام بالمهمة مباشرة
  resolve(123); // أظهر مباشرة النتيجة: 123
});

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

هذا جيد، فعندها يجب أن ننجز الوعد فورًا.

ملاحظة: الحالة state و النتيجة result الداخليتين تكون خصائص الحالة state و النتيجة result لكائن الوعد داخلية. ولا يمكننا الوصول إليهم مباشرة. يمكننا استخدام التوابِع ‎.then/.catch/.finally لذلك والتي سنشرحُها أدناه.

الاستهلاك: عبارات then وcatch وfinally

كائن الوعد هو كالوصلة بين المُنفِّذ (أي ”الشيفرة المُنتِجة“ أو ”المغنّي“) والدوال المُستهلكة (أي ”المُعجبون“) التي ستسلم الناتج أو الخطأ. يمكن تسجيل دوال الاستهلاك (أو أن تشترك، كما في المثال العملي ذاك) باستعمال التوابِع ‎.then و‎.catch و‎.finally.

then

يُعدّ ‎.then أهمّها وعِماد القصة كلها. صياغته هي:

promise.then(
  function(result) { /* نتعامل مع الناتج الصحيح */ },
  function(error) { /* نتعامل مع الخطأ */ }
);

الوسيط الأوّل من التابِع ‎.then يُعدّ دالة تُشغّل إن تحقّق الوعد، ويكون الوسيطُ الناتج.

بينما الوسيط الثاني يُعدّ دالةً تُشغّل إن رُفض الوعد، ويكون الوسيطُ الخطأ.

إليك مثال نتعامل فيه مع وعد تحقّق بنجاح:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done!"), 1000);
});

// ‫تُنفِّذ resolve أول دالة في ‎.then
promise.then(

  result => alert(result), // ‫ إظهار "done!‎" بعد ثانية

  error => alert(error) // هذا لا يعمل
);

هكذا نرى الدالة الأولى هي التي نُفّذت.

وإليك المثال في حالة الرفض:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// ‫تُنفِّذ reject ثاني دالة في ‎.then
promise.then(
  result => alert(result), // لا تعمل

  error => alert(error) // ‫ إظهار "Error: Whoops!‎" بعد ثانية

);

لو لم نُرِد إلّا حالات الانتهاء الناجحة، فيمكن أن نقدّم دالةً واحدة وسيطًا إلى ‎.then فقط:

let promise = new Promise(resolve => {
  setTimeout(() => resolve("done!"), 1000);
});


promise.then(alert); // ‫ إظهار "done!‎" بعد ثانية

catch

لو لم نكن نهتمّ إلّا بالأخطاء، فعلينا استعمال null وسيطًا أولًا: ‎.then(null, errorHandlingFunction)‎، أو نستعمل ‎.catch(errorHandlingFunction)‎ وهو يؤدّي ذات المبدأ تمامًا ولا فرق إلّا قصر الثانية مقارنة بالأولى:

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});


// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // ‫إظهار "Error: Whoops!‎" بعد ثانية

finally

كما المُنغلِقة finally في عبارات try {...} catch {...}‎ العادية، فهناك مثلها في الوعود.

استدعاء ‎.finally(f)‎ يشبه استدعاء ‎.then(f, f)‎، ووجه الشبه هو أنّ الدالة f تعمل دومًا متى استقر الوعد أو أنجز سواءً كان قد تحقّق أو نُكث.

استعمال finally مفيد جدًا لتنظيف ما تبقّى من أمور مهمًا كان ناتج الوعد، مثل إيقاف أيقونات التحميل (فلم نعد نحتاجها). هكذا مثلًا:

new Promise((resolve, reject) => {
  // ‫افعل شيئًا يستغرق وقتًا ثم استدع resolve/reject lre 
})

  // runs when the promise is settled, doesn't matter successfully or not
  .finally(() => stop loading indicator)

  .then(result => show result, err => show error)

ولكنها ليست متطابقة تمامًا مع then(f,f)‎، فهناك فروقات مهمّة:

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

ثانيًا، يمرُّ مُعالج finally على النتائج والأخطاء وبعدها إلى المعالج التالي. مثال على ذلك هو الناتج الذي تمرّر من finally إلى then هنا:

new Promise((resolve, reject) => {
  setTimeout(() => resolve("result"), 2000)
})
  .finally(() => alert("Promise ready"))
  .then(result => alert(result)); // <-- ‫‎.then ستعالج الناتج

كما ترى القيمة value المعادة من الوعد تُمرر عبر finally إلى then، وهذا السلوك مفيد جدًا إذ لا يفترض بأن تتعامل finally مع ناتج الوعد، بل تمرّره إلى من يتعامل معه، وهي مخصصة إلى إجراء عمليات ختامية معينة (مثل التنظيف) بغض النظر عن الناتج.

وهنا واجه الوعد خطأً، وتمرّر من finally إلى catch:

new Promise((resolve, reject) => {
  throw new Error("error");
})
  .finally(() => alert("Promise ready"))
  .catch(err => alert(err));  // <-- ‫‎.catch ستعالج كائن الخطأ error object

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

الخلاصة، المعالج finally:

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

ملاحظة: في الوعود المنجزة المُعالجات تعمل مباشرة إن كان الوعد مُعلقًا لسببٍ ما، فإن معالجات ‎.then/catch/finally ستنتظره. عدا ذلك (إن كان الوعد مُنجزًا) فإن المعالجات ستنفذّ مباشرةً:

// يصبح الوعد منجزًا ومتحققًا بعد الإنشاء مباشرةً
let promise = new Promise(resolve => resolve("done!"));

promise.then(alert); // done! (تظهر الآن)

الآن لنرى أمثلة عملية على فائدة الوعود في كتابة الشيفرات غير المتزامنة.

تحميل السكربتات: الدالة loadScript

أمامنا من الفصل الماضي الدالة loadScript لتحميل السكربتات.

إليك الدالة بطريقة ردود النداء، لنتذكّرها لا أكثر ولا أقل:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  // خطأ في تحميل السكربت كذا
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

هيًا نُعد كتابتها باستعمال الوعود.

لن تطلب دالة loadScript الجديدة أيّ ردود نداء، بل ستصنع كائن وعد يتحقّق متى اكتمل التحميل، وتُعيده. يمكن للشيفرة الخارجية إضافة الدوال المُعالجة (أي دوال الاشتراك) إليها باستعمال .then:

function loadScript(src) {  
  return new Promise(function(resolve, reject) {
    let script = document.createElement('script');
    script.src = src;

    script.onload = () => resolve(script);
    script.onerror = () => reject(new Error(`Script load error for ${src}`));

    document.head.append(script);
  });
}

الاستعمال:

let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");

promise.then(
  script => alert(`${script.src} is loaded!`),
  error => alert(`Error: ${error.message}`)
);

promise.then(script => alert('Another handler...'));

بنظرة خاطفة يمكن أن نرى فوائد هذه الطريقة موازنةً بطريقة ردود النداء:

الوعود ردود النداء
تتيح لنا الوعود تنفيذ الأمور بترتيبها الطبيعي أولًا نشغّل loadScript(script)‎ ومن بعدها ‎.then نكتب ما نريد فعله بالنتيجة. يجب أن يكون تابِع callback تحت تصرفنا عند استدعاء loadScript(script, callback)‎. بعبارة أخرى يجب أن نعرف ما سنفعله بالنتيجة قبل استدعاء loadScript.
يمكننا استدعاء ‎.then في الوعد عدة مرات كما نريد. في كلّ مرة نضيف معجب جديدة "fan"، هنالك تابع سيضيف مشتركين جُدد إلى قائمة المشتركين. سنرى المزيد حول هذا الأمر في الفصل القادم يمكن أن يكون هنالك ردّ واحد فقط.

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

تمارين

إعادة … الوعد؟

ما ناتج الشيفرة أدناه؟

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

  setTimeout(() => resolve(2), 1000);
});

promise.then(alert);

الحل

الناتج هو: 1.

يُهمل استدعاء resolve الثاني إذ لا يتهمّ المحرّك إلّا بأول استدعاء من reject/resolve، والباقي كلّه يُهمل.

التأخير باستعمال الوعود

تستعمل الدالة المضمّنة في اللغة setTimeout ردودَ النداء. اصنع واحدة تستعمل الوعود.

على الدالة delay(ms)‎ إعادة وعد ويجب أن … هذا الوعد خلال ms مليثانية، ونُضيف تابِع .then إليه هكذا:

function delay(ms) {
  // شيفرتك هنا
}

delay(3000).then(() => alert('runs after 3 seconds'));

الحل

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

delay(3000).then(() => alert('runs after 3 seconds'));

لاحظ أنّنا في هذا التمرين استدعينا resolve بلا وسطاء، ولم نُعد أيّ قيمة من delay بل … فقط

صورة دائرة متحركة مع وعد

أعِد كتابة الدالة showCircle في حلّ التمرين السابق لتُعيد وعدًا بدل أن تستلم ردّ نداء. ويكون استعمالها الجديد هكذا:

showCircle(150, 150, 100).then(div => {
  div.classList.add('message-ball');
  div.append("Hello, world!");
});

ليكن الحلّ في التمرين المذكور أساس المسألة الآن.

الحل

يمكنك مشاهدة الحل عبر المثال الحي.

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


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

أفضل التعليقات

Hello @Hasoub Academy I didn't understand this sentence

فعلينا استعمال null وسيطًا أولًا: ‎.then(null, errorHandlingFunction) 

how we can use null value as a mediator 

I have a suggestion about the mood of the website : if you can add an option of add dark mode button.

رابط هذا التعليق
شارك على الشبكات الإجتماعية

بتاريخ On 16‏/11‏/2023 at 00:29 قال دادي Dadi:

Hello @Hasoub Academy I didn't understand this sentence

فعلينا استعمال null وسيطًا أولًا: ‎.then(null, errorHandlingFunction) 

how we can use null value as a mediator 

I have a suggestion about the mood of the website : if you can add an option of add dark mode button.

مرحباً

بالنسبة لشرح الجملة والمقصود بها. هو كالتالي

عند التعامل مع Promises، يمكننا استخدام null في تعبير .then(null, errorHandlingFunction) للدلالة على أننا لا نرغب في التعامل مع الحالة الناجحة (النجاح) للPromise، ونريد التركيز فقط على التعامل مع الأخطاء.

يعني استخدام null كوسيط (الوسيط الأول) هنا أننا لا نهتم بالتعامل مع الحالات النجاحة، ونريد مباشرة التوجه إلى وظيفة التعامل مع الأخطاء.

وبالنسبة للإقتراح سيتم توصيله إلى الإدارة

شكراً لك.

بالتوفيق

رابط هذا التعليق
شارك على الشبكات الإجتماعية



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...