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

استخدام الوعود Promises في جافا سكريبت


ابراهيم الخضور

تُعد فكرة الوعود Promises أساسًا للغة جافا سكريبت غير المتزامنة. والوعد هو كائن يُعاد من الدالة غير المتزامنة ويمثّل الوضع الراهن للعملية. ولا تكون العملية قد انتهت بعد في الوقت الذي تعيد في الدالة الوعد إلى مستدعيها، لكن كائن الوعد المُعَاد يمتلك توابع لمعالجة التنفيذ الناجح أو المخفق للعملية.

تحدثنا في المقال السابق عن استخدام الاستدعاءات لإنجاز الدوال غير المتزامنة. إذ نستدعي وفق هذا اﻷسلوب الدالة غير المتزامنة ممررين إليها دالة استدعاء أخرى تسمى دالة رد النداء callback، عندها تُعيد هذه الدالة قيمتها مباشرة، ثم تستدعي بعد ذلك دالة رد النداء التي مررناها عندما تنتهي العملية.

وما يحدث في الواجهات البرمجية المبنية على الوعود، أن الدالة غير المتزامنة تبدأ عملية ما وتعيد كائن وعد Promise، وبإمكانك حينها ربط هذا الكائن بمعالجات أحداث ستُنفَّذ عند نجاح أو إخفاق هذه العملية.

استخدام الواجهة البرمجية fetch

ملاحظة: سنتعلم مفهوم الوعود في هذا المقال بنسخ عينات من الكود البرمجي من الصفحة إلى طرفية جافا سكريبت في المتصفح. وﻹعداد هذا اﻷمر:

  1. انتقل إلى الموقع: https://example.org
  2. افتح طرفية جافا سكريبت الموجودة ضمن أدوات مطوري الويب في نفس النافذة الفرعية.
  3. عندما نعرض مثالًا ما، انسخه إلى الطرفية، وعليك حينها إعادة تحميل الصفحة في كل مرة تُلصق فيها مثالًا جديدًا، وإلا تعترض الطرفية لأنك أعدت تصريح المتغير 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…");

ما فعلناه في هذه الشيفرة هو التالي:

  1. استدعينا الواجهة()fetchوأسندنا القيمة التي تعيدها إلى المتغير fetchPromise.
  2. سجلنا بعد ذلك مباشرة قيمة المتغير، ومن المفترض أن تشبه النتيجة ما يلي: Promise { <state>: "pending" }، لتخبرنا أنه لدينا كائن وعد Promise له حالة قيمتها "pending" ويعني ذلك أن عملية إحضار الملف لا تزال قيد التنفيذ.
  3. مررنا دالة معالجة إلى التابع ()then العائد لكائن الوعد، فإن نجحت العملية وعندما تنتهي، يستدعي الوعد دالة المعالجة التي نمرر إليها كائن الاستجابة Response الذي يضم استجابة الخادم.
  4. طبعنا الرسالة "…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

اقرأ أيضًا:


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

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

لا توجد أية تعليقات بعد



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...