لنقل بأنّك أنت هو عبد الحليم حافظ، ولنفترض بأنّ مُعجبوك من المحيط إلى الخليج يسألونك ليلًا نهارًا عن الأغنية الشاعرية التالية.
وكي تُريح بالك تعدهم بإرسالها إليهم ما إن تُنشر. فتُعطي مُعجبيك قائمة يملؤون فيها عناوين بريدهم. ومتى ما نشرت الأغنية يستلمها كلّ من في تلك القائمة. ولو حصل مكروه (لا سمح الله) مثل أن شبّت النار والتهمت الأستديو ولم تقدر على نشر الأغنية - لو حصل ذلك فسيعلمون به أيضًا.
وعاش الجميع بسعادة وهناء: أنت إذ لا يُزعجك الجميع بالتهديدات والتوعّدات، ومُعجبيك إذ لن تفوتهم أيّة رائعة من روائعك الفنية.
إليك ما يشبه الأمور التي نفعلها في الحياة الواقعية - في الحياة البرمجية:
- ”شيفرة مُنتِجة“ تُنفّذ شيئًا وتأخذ الوقت. مثل الشيفرات التي تُحمّل البيانات عبر الشبكة. هذا أنت، ”المغنّي“.
- ”شيفرة مُستهلِكة“ تطلب ناتج ”الشيفرة المُنتِجة“ ما إن يجهز. وهناك عديد من الدوال تحتاج إلى هذا الناتج. هذه ”مُعجبوك“.
- الوعد (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
ليصير بإحدى الحالات الآتية:
سنرى لاحقًا كيف سيشترك ”مُعجبونا“ بهذه التغييرات.
إليك مثالًا عن بانيًا للوعود ودالة مُنفِّذ بسيطة فيها ”شيفرة مُنتجِة“ تأخذ بعض الوقت (باستعمال setTimeout
?
let promise = new Promise(function(resolve, reject) { // تُنفّ الدالة مباشرةً ما إن يُصنع الوعد // وبعد ثانية واحدة نبعث بإشارة بأنّ المهمة انتهت والنتيجة هي ”تمت“ (done) setTimeout(() => resolve("done"), 1000); });
بتشغيل الشيفرة أعلاه، نرى أمرين اثنين:
-
يُستدعى المُنفِّذ تلقائيًا ومباشرةً (عند استعمال
new Promise
). -
يستلم المُنفِّذ وسيطين: دالة الحلّ
resolve
ودالة الرفضreject
، وهي دوال معرّفة مسبقًا في محرّك جافاسكربت، ولا داعٍ بأن نصنعها نحن، بل استدعاء واحدة ما إن تجهز النتيجة.
بعد سنة من عملية ”المعالجة“ يستدعي المُنفِّذ الدالةَ resolve("done")
لتُنتج الناتج. هكذا تتغيّر حالة كائن promise
:
كان هذا مثالًا عن مهمّة اكتملت بنجاح، أو ”وعد تحقّق“.
والآن سنرى مثالًا عن مُنفِّذ يرفض الوعد مُعيدًا خطأً:
let promise = new Promise(function(resolve, reject) { // بعد ثانية واحدة نبعث بإشارة بأنّ المهمة انتهت ونُعيد خطأً setTimeout(() => reject(new Error("Whoops!")), 1000); });
باستدعاء reject(...)
ننقل حالة كائن الوعد إلى حالة الرفض "rejected"
:
ملخّص القول هو أنّ على المُنفِّذ تنفيذ المهمة (أي ما يأخذ بعض الوقت ليكتمل) وثمّ يستدعي واحدةً من الدالتين 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
أفضل التعليقات
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.