تُعد فكرة الوعود Promises أساسًا للغة جافا سكريبت غير المتزامنة. والوعد هو كائن يُعاد من الدالة غير المتزامنة ويمثّل الوضع الراهن للعملية. ولا تكون العملية قد انتهت بعد في الوقت الذي تعيد في الدالة الوعد إلى مستدعيها، لكن كائن الوعد المُعَاد يمتلك توابع لمعالجة التنفيذ الناجح أو المخفق للعملية.
تحدثنا في المقال السابق عن استخدام الاستدعاءات لإنجاز الدوال غير المتزامنة. إذ نستدعي وفق هذا اﻷسلوب الدالة غير المتزامنة ممررين إليها دالة استدعاء أخرى تسمى دالة رد النداء callback، عندها تُعيد هذه الدالة قيمتها مباشرة، ثم تستدعي بعد ذلك دالة رد النداء التي مررناها عندما تنتهي العملية.
وما يحدث في الواجهات البرمجية المبنية على الوعود، أن الدالة غير المتزامنة تبدأ عملية ما وتعيد كائن وعد Promise
، وبإمكانك حينها ربط هذا الكائن بمعالجات أحداث ستُنفَّذ عند نجاح أو إخفاق هذه العملية.
استخدام الواجهة البرمجية fetch
ملاحظة: سنتعلم مفهوم الوعود في هذا المقال بنسخ عينات من الكود البرمجي من الصفحة إلى طرفية جافا سكريبت في المتصفح. وﻹعداد هذا اﻷمر:
- انتقل إلى الموقع: https://example.org
- افتح طرفية جافا سكريبت الموجودة ضمن أدوات مطوري الويب في نفس النافذة الفرعية.
-
عندما نعرض مثالًا ما، انسخه إلى الطرفية، وعليك حينها إعادة تحميل الصفحة في كل مرة تُلصق فيها مثالًا جديدًا، وإلا تعترض الطرفية لأنك أعدت تصريح المتغير
fetchPromise
.
سننزل في هذا المثال ملف JSON ونسجّل بعض المعلومات المتعلقة به، ولتنفيذ اﻷمر نرسل طلب HTTP إلى الخادم يتضمن رسالة مرسلة إليه وننتظر الاستجابة. ففي مثالنا سنطلب ملف JSON من الخادم. وكما أشرنا في المقال السابق، نستخدم الواجهة البرمجية XMLHttpRequest
لتنفيذ طلبات HTTP، لكننا سنستخدم في هذا المقال الواجهة البرمجية ()fetch
وهي بديل أحدث عن كائن XMLHttpRequest
ومبنية على الوعود.
انسخ اﻵن الشيفرة التالية إلى طرفية جافا سكريبت في متصفحك:
const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); console.log(fetchPromise); fetchPromise.then((response) => { console.log(`Received response: ${response.status}`); }); console.log("Started request…");
ما فعلناه في هذه الشيفرة هو التالي:
-
استدعينا الواجهة
()fetch
وأسندنا القيمة التي تعيدها إلى المتغيرfetchPromise
. -
سجلنا بعد ذلك مباشرة قيمة المتغير، ومن المفترض أن تشبه النتيجة ما يلي:
Promise { <state>: "pending" }
، لتخبرنا أنه لدينا كائن وعدPromise
له حالة قيمتها"pending"
ويعني ذلك أن عملية إحضار الملف لا تزال قيد التنفيذ. -
مررنا دالة معالجة إلى التابع
()then
العائد لكائن الوعد، فإن نجحت العملية وعندما تنتهي، يستدعي الوعد دالة المعالجة التي نمرر إليها كائن الاستجابةResponse
الذي يضم استجابة الخادم. - طبعنا الرسالة "…Started request" لتدل على أننا بدأنا تنفيذ الطلب.
من المفترض أن يكون خرج الشيفرة السابقة كالتالي:
Promise { <state>: "pending" } Started request… Received response: 200
لاحظ كيف ظهرت العبارة "Started request" على الشاشة قبل تلقي الاستجابة. فعلى خلاف الدوال المتزامنة، تعيد()fetch
قيمتها قبل أن يكتمل الطلب، مما يسمح للبرنامج بمتابعة التنفيذ. ثم يعيد البرنامج بعد ذلك رمز الحالة 200
ويعني أن الطلب قد نُفِّذ بنجاح.
قد يبدو هذا المثال مشابهًا للمثال في المقال السابق الذي استخدمنا فيه معالجات أحداث على الكائن XMLHttpRequest
، لكننا مررنا هذه المرة دالة معالجة إلى التابع ()then
العائد لكائن الوعد الذي تعيده الدالة()fetch
.
سلسلة من الوعود
بمجرد حصولك على كائن استجابة Response
باستخدام الواجهة()fetch
، عليك استدعاء دالة أخرى للحصول على بيانات الاستجابة، ونريدها في هذه الحالة على هيئة بيانات JSON لهذا نستدعي التابع ()json
العائد للكائن Respond
. وكذلك الأمر، التابع ()json
غير متزامن، هنا سنكون أمام حالة نستدعي فيها دالتين غير متزامنتين على التوالي.
جرّب اﻵن ما يلي:
const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise.then((response) => { const jsonPromise = response.json(); jsonPromise.then((data) => { console.log(data[0].name); }); });
أضفنا في هذا المثال أيضًا التابع ()then
إلى الوعد الذي تعيده ()fetch
. لكن المعالج سيستدعي هذه المرة()response.json
ومن ثم يمرر معالج ()then
جديد إلى الوعد الذي يعيده التابع ()response.json
.
يُفترض في هذه الحالة طباعة العبارة في الطرفية (وهو اسم أول منتج في القائمة التي يضمها الملف "products.json"). لكن مقارنة مع الاستدعاءات التي شرحناها في مقال سابق، سنجد أننا نستدعي التابع ()then
ضمن تابع ()then
آخر وهذا مشابهة لفكرة استدعاء دالة استدعاء ضمن دالة استدعاء بشكل متعاقب، وكنا قد قلنا بأن هذا اﻷمر سيزيد من صعوبة قراءة الشيفرة وفهمها، وأطلقنا عليه اسم "جحيم الاستدعاء callback hell"، وما يحدث هنا أمر مشابه لكن مع الدالة ()then
!
نعم، اﻷمر نفسه تمامًا، لكن الوعد يقدم ميزة خاصة وهي أن التابع ()then
يعيد هو أيضًا وعدًا يكتمل بنتيجة الدالة التي مُرر إليها. لهذا من اﻷفضل إعادة كتابة المثال السابق كالتالي:
const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => response.json()) .then((data) => { console.log(data[0].name); });
فبدلًا من استدعاء تابع ()then
آخر ضمن معالج التابع ()then
الأول، يمكننا إعادة الوعد الذي يعيده التابع ()json
ثم نستدعي التابع ()then
الثاني على القيمة المُعادة. يُدعى هذا اﻷمر بسلسلة الوعود Promise chaining، ونستطيع من خلال هذه الميزة تفادي زيادة مستوى التداخل عندما نضطر إلى استدعاء الدوال غير المتزامنة بشكل متتالٍ.
قبل الانتقال إلى الخطوة التالية، علينا إضافة شيء آخر، وهو التأكد من قبول الخادم للطلب وقدرته على معالجته قبل أن نحاول قراءة الاستجابة، ولتنفيذ اﻷمر نتحقق من رمز حالة الطلب ونعرض خطأ إن لم يكن رمز الحالة 200
(أو OK
)
const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.json(); }) .then((data) => { console.log(data[0].name); });
التقاط اﻷخطاء
وصلنا حاليًا إلى الجزء اﻷخير المتعلق بمعالجة الأخطاء. إذ يمكن للواجهة البرمجية ()fetch
رمي أخطاء لأسباب عديدة مثل عدم وجود اتصال بالشبكة أو تمرير عنوان URL غير صالح وغيرها من الأسباب، وبإمكاننا أيضًا رمي أخطاء بأنفسنا إن أعاد الخادم حالة خطأ. ورأينا في المقال السابق أن معالجة اﻷخطاء قد يغدو صعبًا عند تداخل الاستدعاءات ويدفعنا لمعالجة اﻷخطاء في كل مستوى على حدة.
ولمعالجة اﻷخطاء، يقدّم الكائن Promise
التابع ()catch
الذي يشبه كثيرًا التابع ()then
من حيث استدعاؤه وتمريره إلى دالة المعالجة. لكن ما يحدث أن استدعاء التابع ()catch
يكون عند إخفاق العملية غير المتزامنة وليس نجاحها. فلو أضفت ()catch
إلى نهاية سلسلة الوعود، سيُستدعى هذا التابع إن أخفقت أية دالة غير متزامنة في السلسلة. أي بإمكانك إنجاز أي عملية غير متزامنة على شكل سلسلة من الدوال غير المتزامنة المتتابعة التي تنتهي بتابع يعالج جميع اﻷخطاء.
جرّب هذه النسخة التي تستخدم ()catch
وتُعدّل عنوان URL حتى تُخفق العملية:
const fetchPromise = fetch( "bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.json(); }) .then((data) => { console.log(data[0].name); }) .catch((error) => { console.error(`Could not get products: ${error}`); });
نفّذ الشيفرة السابقة، ومن المفترض أن ترى الخطأ الذي يعرضه التابع ()catch
.
المصطلحات الخاصة بالوعود
تستخدم الوعود مجموعة مخصصة من المصطلحات التي لا بد من توضيحها.
بداية للوعد حالة من ثلاث حالات وهي كالتالي:
-
قيد التنفيذ pending: يُنشأ كائن الوعد في هذه الحالة، لكن الدالة غير المتزامنة المرتبطة به لم تنجح أو تخفق بعد. وتوافق هذه الحالة إعادة الوعد إلى الدالة بعد استدعاء
()fetch
بينما لا يزال الطلب قيد التنفيذ. -
منجز fulfilled: وهي حالة نجاح العملية غير المتزامنة، ويُستدعى حينها التابع
()then
. -
مرفوض rejected: وهي حالة إخفاق العملية غير المتزامنة، ويُستدعى عندها التابع
()catch
.
أما معنى "النجاح" أو "اﻹخفاق" فيعود للواجهة البرمجية المستخدمة. فالواجهة()fetch
مثلًا ترفض الوعد المعاد لأسباب منها خطأ في الشبكة يمنع إرسال الطلب، وتنجزه عندما يعيد الخادم الاستجابة حتى لو كانت اﻹستجابة حالة خطأ مثل 404 Not Found.
كما نستخدم أيضًا مصطلح "مسوّىً settled" ليشير إلى حالتي الرفض أو اﻹنجاز. ونقول عن الوعد أنه "مقضي resolved" إن جرت "تسويته settled" أو كان "مقفلًا locked in" بانتظار حالة وعد آخر.
الجمع بين عدة وعود
إن تكوّنت العملية غير المتزامنة من عدة دوال نستخدم حينها سلسلة من الوعود، ولا بد حينها من تسوية كل وعد قبل الانتقال إلى اﻵخر. لكنك قد تحتاج أحيانًا إلى الجمع بين عدة استدعاءات لدوال غير المتزامنة، لهذا تزوّدك الواجهة البرمجية ()Promis
ببعض الدوال المساعدة.
قد يتطلب الأمر في بعض التطبيقات إنجاز عدة وعود لا تتعلق ببعضها البعض. والطريقة اﻷكثر فعالية لتنفيذ هذا اﻷمر هي استدعاء جميع الدوال في نفس الوقت، ثم الحصول على تنبيه عندما تنجز جميعها، وهذا ما يقدمه التابع ()Promise.all
فهو يُعيد مصفوفة من الوعود كما يعيد وعدًا واحدًا، ويكون هذا الوعد:
-
منجزَا: عندما تُنجز كل الوعود في المصفوفة. ويُستدعى عند ذلك معالج التابع
()then
وتُمرّر له مصفوفة من الاستجابات وبنفس ترتيب الوعود التي مُررت إلى التابع()all.
-
مرفوضًا: إن رٌفض أي من وعود المصفوفة، ويُستدعى عند ذلك معالج التابع
()catch
ويُمرر له الخطأ الناتج عن الوعد الذي رُفض.
إليك مثالًا:
const fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.all([fetchPromise1, fetchPromise2, fetchPromise3]) .then((responses) => { for (const response of responses) { console.log(`${response.url}: ${response.status}`); } }) .catch((error) => { console.error(`Failed to fetch: ${error}`); });
نفّذنا في الشيفرة السابقة ثلاثة طلبات()fetch
إلى ثلاثة عناوين URL مختلفة، فإن نجحت هذه الطلبات، نعرض حالة الاستجابة لكل طلب، وإن أخفقت إحداها، نطبع الخطأ.
ينبغي أن تُنجز الطلبات جميعها لأن العناوين صحيحة، مع ملاحظة أن رقم الحالة للطلب الثاني سيكون 404
بدلًا من 200
لأن الملف الذي نطلبه غير موجود حقيقة. لهذا سيكون خرج الكود السابق على النحو التالي:
https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json: 200 https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found: 404 https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json: 200
جرّب أن تنفّذ الشيفرة السابقة بعد كتابة العناوين بشكل خاطئ:
const fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "bad-scheme://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.all([fetchPromise1, fetchPromise2, fetchPromise3]) .then((responses) => { for (const response of responses) { console.log(`${response.url}: ${response.status}`); } }) .catch((error) => { console.error(`Failed to fetch: ${error}`); });
نتوقع اﻵن أن تُنفَّذ دالة المعالجة الخاصة بالتابع ()catch
، ومن المفترض حينها أن يكون الخرج مشابهًا للتالي:
Failed to fetch: TypeError: Failed to fetch
وقد تريد في بعض الحالات أن يُنجز أحد الوعود ولا يهم أيها، عندها يمكنك الاستفادة من التابع ()Promise.any
الذي يشابه ()Promise.all
لكنه يُنجز بمجرد إنجاز أي وعد في مصفوفة الوعود، ويُرفض إن رُفضت جميع الوعود:
const fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.any([fetchPromise1, fetchPromise2, fetchPromise3]) .then((response) => { console.log(`${response.url}: ${response.status}`); }) .catch((error) => { console.error(`Failed to fetch: ${error}`); });
ملاحظة: لا يُمكن في هذه الحالة توقّع أي وعد سيُنجز أولًا.
للتعرف على بقية التوابع التي يمكن استخدامها للجمع بين الوعود راجع توثيق ()Promis
.
استخدام التعليمتين async
و await
تُسهّل التعليمة async
عمل الشيفرة غير المتزامنة التي تعتمد على الوعود، فإضافة هذه التعليمة إلى بداية الدالة يجعلها دالة غير متزامنة:
async function myFunction() { // This is an async function }
وباﻹمكان استخدام التعليمة await
قبل استدعاء الدالة التي تعيد وعدًا. وهذا ما يجعل الشيفرة تنتظر عند هذه النقطة حتى يسوّى الوعد وعندها تُعد قيمة الوعد المنجز هي القيمة المعادة من قبل الدالة أو ترمي الدالة قيمة الوعد المرفوض كخطأ.
وهكذا ستتمكن من كتابة شيفرة غير متزامنة مع أنها تبدو كذلك. لهذا سنحاول كتابة مثال()fetch
كالتالي:
async function fetchProducts() { try { //`()fetch` تنتظر الدالة بعد هذا السطر حتى يسوّى الاستدعاء // الذي سيُعسد استجابة أو يرمي خطأ const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } //`response.json()` تنتظر الدالة بعد هذا السطر حتى يسوّى الاستدعاء //أو يرمي خطأ JSON الذي يعيد كائن const data = await response.json(); console.log(data[0].name); } catch (error) { console.error(`Could not get products: ${error}`); } } fetchProducts();
نستدعي هنا الدالة ()await fetch
وسنحصل على كائن استجابة Response
مكتمل بدلًا من الوعد ()Promis
وكأن()fetch
دالة متزامنة.
ونستطيع أيضًا استخدام الكتلة try...catch
لمعالجة اﻷخطاء كما لو كنا نكتب شيفرة متزامنة. وتذكر أن الدوال غير المتزامنة تُعيد وعدًا دائمًا، لهذا لا يمكن أن نكتب شيفرة كهذه:
async function fetchProducts() { try { const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error(`Could not get products: ${error}`); } } const promise = fetchProducts(); console.log(promise[0].name); //هو كائن وعد فلن تعمل هذه الشيفرة "promise"
ويجب عليك تصحيح الكود السابق على النحو التالي:
async function fetchProducts() { try { const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error(`Could not get products: ${error}`); } } const promise = fetchProducts(); promise.then((data) => console.log(data[0].name));
وتذكر أن استخدام await
يكون ضمن دالة، إلا في الحالة التي تكون فيها الشيفرة ضمن وحدة JavaScript module وليس ضمن سكريبت نمطي:
try { // using await outside an async function is only allowed in a module const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); console.log(data[0].name); } catch (error) { console.error(`Could not get products: ${error}`); }
قد تستخدم دوال async
كثيرًا مقارنة باستخدام سلسلة الوعود لكونها تجعل العمل مع الوعود أكثر وضوحًا. وتذكر أن await
-كما هو حال سلسلة الوعود- تجبر العمليات المتزامنة على التنفيذ المتسلسل، وهذا أمر ضروري إن اعتمدت نتيجة العملية الثانية على سابقتها، أما إن لم يكن الوضع كذلك، ففكرّ في هذه الحالة باستخدام الدالة ()Promise.all
.
الخلاصة
تُعرفنا في مقال اليوم على مفهوم الوعود التي تعد أساسًا للبرمجة غير المتزامنة في جافا سكريبت الحديثة. إذ تجعل تسلسل العمليات غير المتزامنة أكثر وضوحًا وإدراكًا بدلًا من التداخل المفرط للاستدعاءات، كما تدعم نمطًا من معالجة الأخطاء مشابهًا ﻵلية try...catch
.
وتسّهل التعليمتان async
و await
بناء عمليات باستدعاء سلسلة من الدوال غير المتزامنة المتتابعة دون الحاجة إلى سلاسل صريحة من الوعود وكتابة شيفرة شبيهة بالشيفرة المتزامنة.
تعمل الوعود في النسخ اﻷخيرة من معظم المتصفحات الحديثة، وستكون المشكلة فقط مع متصفحي أوبرا ميني Opera mini وإنترنت إكسبلورر 11 والنسخ اﻷقدم.
لم نناقش بالتأكيد كل ميزات الوعود في مقالنا الحالي، بل سلطنا الضوء على الميزات اﻷكثر أهمية واستخدامًا، وستتعلم خلال مسيرتك في تعلم جافا سكريبت الكثير من الميزات والتقنيات المفيدة الأخرى، ويجدر بالذكر أن الكثير من واجهات الويب البرمجية مبنية أساسًا على الوعود مثل WebRTC و Web Audio API Media Capture and Streams API وغيرها الكثير.
ترجمة -وبتصرف- للمقال: How to use promises
اقرأ أيضًا:
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.