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

السلام عليكم ورحمة الله وبركاته

وقت القراءة: 18 دقيقة

المقدمة

في هذه المقالة سنتحدث عن الـ promise لما هي موجودة وما فائدتها وكيف نتعامل معها وكيف ننشيء promise خاص بنا

ما هو الـ Promise

هو object يحتوي على دوال وخصائص متنوعة يتعامل مع البيانات التي تستغرق وقتًا
المعنى الحرفي له هو وعد أي انه يعدك بالحصول على نتيجة

أحيانًا يكون هناك عمليات تحتاج لوقت لكي تنتهي فمثلًا جلب بيانات من API أو قراءة ملف فهذه عمليات قد تستغرق وقتًا

فالـ promise هنا يستطيع تولي هذه الأمور وعندما ينتهي من تنفيذ العملية سيخبرك الـ promise أنه انتهى

الوعي بالمشكلة

تذكر أنك هنا أنت لا تعرف ولن تستطيع أن تعرف الوقت الذي ستنتهي فيه العملية، لأنه ليس وقتًا ثابتًا فيمكن أن يستغرق جلب البيانات من API ثانية أو ثانيتين أو حتى 5 ثواني
لا يوجد وقت ثابت لانه يعتمد على عوامل متغيرة كثيرة

هل هذه مشكلة ؟
حسنًا ان كنت تريد تنفيذ كود ما مباشرةً بعد انتهاء العملية التي ستستغرق وقتً، فكيف ستعرف متى ستنتهى ؟

فمثلًا لدينا صفحة عندما تفتحها تقوم بجلب البيانات من API ثم تعرضها في الصفحة

كيف ستعرف متى سيتم جلب البيانات لكي تعرض البيانات في الصفحة ؟
وهل سيتوقف كامل الموقع عن العمل لحين انتهاء هذه العمليات ؟

هنا أتى الـ promise الذي يستطيع أخذ الكود الذي سيستغرق وقتًا
ثم عندما ينتهي من تنفيذ العملية سيخبرنا الـ promise أنه انتهى

ويمكنك حينها تنفيذ ما تريد ويظل الموقع يعمل بسلاسة دون أن يتأثر بهذه العمليات

استخدام الـ Promise

let ourPromise = new Promise(function (resolve, reject) {});

هنا أنشأنا promise وكما تلاحظ فأنه يأخذ callback function كـ argument كما ترى هنا function (resolve, reject) {}
وهذا الـ callback كما ترى يستقبل متغيرين resolve و reject، يمكنك بالطبع تسميتهما بأي اسم لكن يجب الترتيب مهم طبعًا ويتم تخزين كل هذا في متغير يدعى ourPromise

حسنًا ما هما الـ resolve والـ reject بالتحديد ؟
هما في الحقيقة دوال الأولى هي resolve نستدعيها عندما ينجح الـ promise الخاص بنا ونستدعي reject عندما يحدث أي خطأ

بهذا الشكل

let ourPromise = new Promise(function (resolve, reject) {
  resolve('success'); // call resolve if there is no errors
  reject('there is an error'); // call reject if there is an errors
});

ملحوظة: سنرى فيما بعد ما استخدامات عملية للـ resolve و reject فيما بعد في الأمثلة التالية

نستدعي resolve عندما تنجح العملية ونستطيع إرسال أي معلومات معها سواء جملة أو object أو أي بيانات تريد أن ترسلها، هنا نحن فقط أرسلنا جملة تقول success

وإذا حدث أي خطأ فسنستدعي reject ونرسل أي شيء أيضًا بنفس الفكرة

دعونا نرى مثالا للتوضيح

let ourPromise = new Promise(function (resolve, reject) {
  let isSuccess;

  // code that takes long time ...
  // read file or fetch API

  isSuccess = true; // determinate if it success or fail
  if (isSuccess) resolve('success');
  else reject('fail');
});

حسنًا هذا كود تخيُلي للأمر
لدينا متغير يدعى isSuccess يحدد ما إذا نجحت العملية أو حدث خطأ ما
ثم لدينا كود يستغرق وقتًا طويلًا كما قلنا أي كان الكود

ثم لدينا شرط يفحص الـ isSuccess ويرى إذا لم يحدث خطأ فاستدعي resolve وإذا حدث خطأ فاستدعي reject

حالات الـ Promise

قبل أن نكمل عليك أن تعرف أن هناك ثلاث حالات فقط للـ promise وهم:

مثال توضيحي

لنقول أننا نريد جمع رقمين ولنتظاهر أنها عملية تأخذ وقتًا طويلًا

let ourPromise = new Promise(function (resolve, reject) {
  // this code takes 3 sec
  setTimeout(function () {
    let x = 5;
    let y = 7;
    // reject negative numbers
    if (x < 0 || y < 0) reject('Negative numbers are not accepted');
    else {
      let sum = x + y;
      if (sum > 0) resolve(sum);
    }
  }, 3000); // 3000 ms = 3 second
});

استعملنا setTimeout لنحاكي أن هذه العملية ستستغرق 3 ثواني كمثال
الآن ستلاحظ هنا أننا وضعنا شرط أننا لا نقبل الأعداد السالبة
إذا وجدنا أي عدد سالب فسنستدعي reject ونرسل رسالة توضحية بأننا لا نقبا الأعداد السالبة

واذا كانا الرقمين موجبين فأننا نجمعهما ثم نرسل ناتج الجمع مع resolve عندما ينتهي

تذكر أن الـ promise نخزنه في متغير يدعى ourPromise كما تلاحظ
الآن هل نستطيع الحصول على الناتج عندما نطبع المتغير ourPromise ؟
دعونا نرى

console.log(ourPromise);

الناتج:

Promise {<pending>}

سنتلاحظ انه طبع لك object من نوع Promise يحتوي على حالة pending
أي ان العملية لم تنتهي بعد، حسنًا كيف نعرف متى ستنتهي ؟

هنا يأتي دور دالة داخل الـ Promise تدعى then يتم تفعيلها بعد اكتمال العملية

ourPromise.then(function (result) {
  console.log(result); // OUTPUT: 500500
});

دالة then تستقبل callback وهذا الـ callback معه متغير يحتوي على الناتج الذي ارسلناه في الـ resolve
وتذكر أننا أرسلنا sum داخل الـ resolve بالتالي قيمة الـ result ستكون قيمة الـ sum

بالتالي هنا كما قلنا دالة then سيتم تفعيلها عندما ينتهي الـ promise من العملية

هكذا عندما يكون هناك عملية تعتمد على عملية أخرى وهذه العملية الأخرى أنت لا تعرف متى ستنتهي، فيمكنك حينها وضع العملية الأولى داخل promise ثم تجعل العملية الثانية تنتظر الناتج داخل الـ then

let ourPromise = new Promise(function (resolve, reject) {
  resolve('Success');
});

console.log(ourPromise); // OUTPUT: Promise { 'Success' }

إذا كان ourPromise يقوم بعمل resolve لكلمة Success
وقمنا بطباعة الـ ourPromise
فستجد أنه يرجع لك promise يحتوي على كلمة Success والـ ourPromise يكون fulfilled في هذه الحالة لاننا قمنا بعمل resolve بشكل مباشر

ولكي نحصل على الجملة التي ارسلناها مع الـ resolve يجب أن نستخدم then كما قلنا

ourPromise.then(function (result) {
  console.log(result); // OUTPUT: Success
});

التعامل مع الـ reject والـ catch

الآن فهمنا أن بعد ما تنتهي العملية تقوم بعمل resolve
ثم نحن نعلم متى ستنتهي العملية عن طريق دالة then

الآن ماذا يحدث عندما يحدث reject في الـ promise ؟

لنحاول جعل المتغيرات ونرى ماذا سيحدث

let ourPromise = new Promise(function (resolve, reject) {
  // this code takes 3 sec
  setTimeout(function () {
    let x = -5;
    let y = -7;
    // reject negative numbers
    if (x < 0 || y < 0) reject('Negative numbers are not accepted');
    else {
      let sum = x + y;
      if (sum > 0) resolve(sum);
    }
  }, 3000); // 3000 ms = 3 second
});

هنا الـ x والـ y اصبحها -5 و -7

ثم انتظرنا الـ then بشكل الاعتيادي

ourPromise.then(function (result) {
  console.log(result); // ERROR!!
});

ستجد أمامك رسالة حب جميلة هنا، مفادها

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "Negative numbers are not accepted".] {
  code: 'ERR_UNHANDLED_REJECTION'
}

الرسالة تقول أنه لم يجد شيء يتعامل مع الـ reject
كدالة .catch()

ما معنى هذا ؟ ببساطة الـ then مصممة لتتعامل مع حالات الـ resolve فقط وليس مع الـ reject
لذا يجب أن نتعامل مع الـ reject بشكل مستقل وهنا لدينا حلين أول طريقتين كما قلنا
الحل الأول أن نستعمل دالة تدعى catch
وهو ببساطة أي reject يحدث يذهب إلى دالة الـ catch

ourPromise
  .then(function (result) {
    console.log(result);
  })
  .catch(function (error) {
    console.log(error);
  });

هنا اذا حدث أي result سيتم تفعيل then
واذا حدث أي reject سيتم تفعيل catch

في حالتنا هذه سيتم تفعيل catch الذي يستقبل callback معه متغير يحتوي على الرسالة التي ارسلناها اثناء الـ reject
وكانت Negative numbers are not accepted

مشكلة الـ callback hell مع الـ promise

حسنًا تتذكر صديقنا العزيز callback hell من المقالة الخاصة بالـ callback
سنواجه مجددًا هنا، لكن لا تخف لأن الـ promise لديها طريقة جيدة لحل هذه المشكلة
لكن أولًا لنستعرض شكل المشكلة بمثال وكيف ستكون مع الـ promise

function addTwo(x, y) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(x + y);
    }, 3000);
  });
}

هنا لدينا دالة تدعى addTwo تأخذ قيمتين وتجمعهم
ومثل ما اعتدنا سنقول أن عملية الجمع تستغرق 3 ثواني كمثال
لذا بسبب أنها عملية تستغرق وقتًا فأننا جعلناها داخل promise

دالة addTwo كما تلاحظ فإنها تقوم بإرجاع هذا الـ promise
بمعنى أنه عندما نستدعي الدالة هكذا

addTwo(5, 7); // return a promise (Promise {<pending>})

فانها سترجع لنا promise في حالة pending
بالتالي فيجب أن نستعمل then لكي ننتظر انتهاء العملية

addTwo(5, 7).then(function (result) {
  console.log(result); // OUTPUT: 12 (after 3 second)
});

حتى الآن لا شيء جديد علينا
دالة الـ then ستجمع الرقمين ثم ننتظر الناتج في الـ then بعد 3 ثواني

حسنًا إذا أردنا الآن أن نجمع 10 علي الناتج result
بهذا الشكل

addTwo(5, 7).then(function (result) {
  addTwo(result, 10); // add 10 to the result
});

داخل الـ then استدعينا الدالة addTwo مجددًا لكي نجمع 10 على الناتج كما قلنا
هنا الـ addTwo الثانية سترجع لنا promise لذا سنقوم بعمل then لها

addTwo(5, 7).then(function (result) {
  addTwo(result, 10).then(function (result2) {
    console.log(result2); // OUTPUT: 22 (after 6 second)
  });
});

حسنًا أظنك بدأت تستوعب كيف ستظهر مشكلة الـ callback hell الآن
لنكبر العمليات قليلًا لنضح عملية جمع اخرى للناتج الأخير result2

addTwo(5, 7).then(function (result) {
  addTwo(result, 10).then(function (result2) {
    addTwo(result2, 12).then(function (result3) {
      console.log(result3); // OUTPUT: 34 (after 9 second)
    });
  });
});

ماذا عن وضع 5 عمليات أخرى ؟

addTwo(5, 7).then(function (result) {
  addTwo(result, 10).then(function (result2) {
    addTwo(result2, 12).then(function (result3) {
      addTwo(result3, 11).then(function (result4) {
        addTwo(result4, 20).then(function (result5) {
          addTwo(result5, 32).then(function (result6) {
            addTwo(result6, 18).then(function (result7) {
              addTwo(result7, 99).then(function (result8) {
                console.log(result8); // OUTPUT: 214 (after 24 second)
              });
            });
          });
        });
      });
    });
  });
});

رحب معي بصديقنا المتواضع callback hell بنفسه هنا

حسنًا الآن كيف سنتخلص منه :)

حل مشكلة الـ callback hell في الـ promise

حسنًا لنعود للبداية

addTwo(5, 7).then(function (result) {
  console.log(result); // OUTPUT: 12 (after 3 second)
});

حسنًا قلنا أننا نريد أن نجمع 10 علي الناتج result
ثم استدعينا AddTwo أخرى وربطناها بـ then

هكذا

addTwo(5, 7).then(function (result) {
  addTwo(result, 10).then(function (result2) {
    console.log(result2); // OUTPUT: 22 (after 6 second)
  });
});

هنا بدأت المشكلة بالظهور
الحل بسيط جدًا وهو أن دالة الـ then تملك return ويمكننا إرجاع أي شيء داخلها

هكذا

addTwo(5, 7).then(function (result) {
  return addTwo(result, 10); // this `then` return a promise (Promise {<pending>})
});

ركز معي هنا جيدًا، داخل الـ then أرجعنا دالة الـ addTwo أو بمعنى أخر لقد أرجعنا promise

حسنًا دالة الـ then عندما ترجع لنا promise فيمكننا حينها ربط then أخرى بها

ما معنى هذا ؟ أي يمكننا استدعاء then اخرى من خلال الـ then الأولى

هكذا

addTwo(5, 7)
  .then(function (result) {
    return addTwo(result, 10);
  })
  .then(function (result) {
    console.log(result); // OUTPUT: 22 (after 6 second)
  });

حسنًا عرفنا أن الـ then الأولى أرجعت promise لأن دالة addTwo هى promise بحد ذاتها
فطالما أن الـ then الأولى ترجع promise فيمكننا ربط then أخرى بهذا الـ promise
والـ then الثانية ستستدعى عندما تنتهي العملية في الـ addTwo الثانية التي ارجعناها

لنضيف عملية أخرى

addTwo(5, 7)
  .then(function (result) {
    return addTwo(result, 10);
  })
  .then(function (result) {
    return addTwo(result, 12);
  })
  .then(function (result) {
    console.log(result); // OUTPUT: 34 (after 9 second)
  });

يمكنك أن تستوعب الآن أن كل then ترجع promise بالتالي نستدعي then جديدة لتتعامل مع الـ promise الذي رجع من الـ then السابقة

الكود التالي سيكون كود توضيحي عام مجرد من أي دوال من أجل الشرح والتبسيط

promise_1(...)
  .then(function (...) {
    // this then will wait for `promise_1`
    return promise_2(...);
  })
  .then(function (...) {
    // this then will wait for `promise_2`
    return promise_3(...);
  })
  .then(function (...) {
    ...
  })
  .then(...)
  .then(...)
  ...

الأمر كله يبدأ بـ promise_1 ثم انتظرناه لينتهي في الـ then الأولى
ثم قمنا بإرجاع promise_2 ثم انتظرناه لينتهي في الـ then الثانية
ثم قمنا بإرجاع promise_3 ثم انتظرناه لينتهي في الـ then الثالثة
ثم قمنا بإرجاع ... ثم انتظرناه لينتهي في الـ ...
ثم ... ثم ... ثم ... ثم

يمكنك أن تغنيها :)
كل then تقوم بإرجاع promise وتسلم للبعدها

دعونا نرى المثال الخاص بالـ Promises chaining بهذه الطريقة

Promises chaining

addTwo(5, 7)
  .then(function (result) {
    return addTwo(result, 10);
  })
  .then(function (result) {
    return addTwo(result, 12);
  })
  .then(function (result) {
    return addTwo(result, 11);
  })
  .then(function (result) {
    return addTwo(result, 20);
  })
  .then(function (result) {
    return addTwo(result, 32);
  })
  .then(function (result) {
    return addTwo(result, 18);
  })
  .then(function (result) {
    return addTwo(result, 99);
  })
  .then(function (result) {
    console.log(result); // OUTPUT: 214 (after 24 second)
  });

حسنًا الآن أصبح لدينا مبنى من 8 طوابق، هذا تغير ملحوظ كما ترى
وأفضل من الـ callback
هذا الشكل الذي تراه يسمى Promises chaining وهو اسم يناسبه

يمكنك هنا هدم طابق إن كنت لا تريده ... أقصد يمكنك هنا ازالة أي then بسهولة دون أن تضطر إلى تعديل الباقي

إن كنت تتذكر فكان من الصعب ازالة عملية من المنتصف الـ callback hell لانها تكون مندمجة مع الـ callback التي فوقها والتي تحتها
حاول أن تزيل callback من منتصف الـ callback hell وستفهم المعاناه

يمكنك أن تحفظ شكل الـ Promises chaining هكذا

Promise
  .then(...)
  .then(...)
  .then(...)
  .then(...)
  ...

الـ catch في الـ promises chaining

حسنًا استدعاء دالة الـ catch في نهاية سلسلة الـ then تلك

بهذا الشكل

Promise
  .then(...)
  .then(...)
  .then(...)
  .catch(...) // catch any exception

والـ catch يمكنها استقبال أي throw New Error(...) أو return Promise.reject(...) أو أي خطأ عموما قد يحدث في أي then في السلسلة كـ syntax error أو عدم تعريف متغير وهكذا من الأخطاء المختلفة

سنأخذ بعض الأمثلة لنفترض أننا وضعنا شرط في الـ then الثانية أن الناتج لا يجب أن يتجاوز الـ 20 على سبيل المثال

addTwo(5, 7)
  .then(function (result) {
    return addTwo(result, 10);
  })
  .then(function (result) {
    if (result > 20)
      return Promise.reject('Result that greater than 20 is not accepted');

    return addTwo(result, 12);
  })
  .then(function (result) {
    console.log(result);
  })
  .catch(function (err) {
    console.log(err); // Result that greater than 20 is not accepted
  });

الآن الـ then الثانية سترجع لنا promise معمول له reject
والـ catch بطبيعة الحالة ستستقبل هذا الـ reject وتتعامل معه

Promise
  .then(...) // success
  .then(...) // success
  .then(...) // return promise.reject or throw "error message"
  .then(...) // will be ignored
  .then(...) // will be ignored
  .then(...) // will be ignored
  .catch(...) // handle that reject

عندما يحدث reject في أي then سيتم تجاهل كل الـ then التالية وسيتم القفز لدالة الـ catch مباشرةً

وكما قلنا سابقًا الـ then مصممة لتتعامل مع حالات الـ resolve فقط وليس مع الـ reject
وأي reject يحدث يذهب إلى دالة الـ catch

ولو اردنا استخدام الـ throw فالأمر سواسية

addTwo(5, 7)
  .then(function (result) {
    return addTwo(result, 10);
  })
  .then(function (result) {
    if (result > 20) throw 'Result that greater than 20 is not accepted';

    return addTwo(result, 12);
  })
  .then(function (result) {
    console.log(result);
  })
  .catch(function (err) {
    console.log(err); // Result that greater than 20 is not accepted
  });

الكود هنا بالـ throw ولا يختلف عن سابقه، فقط نرسل الرسالة التي نريدها وسيتم الذهاب إلى الـ catch بنفس رسالة الخطأ

Promise.resolve(...) & Promise.reject(...)

نحن لديناها Promise.resolve(...) التي تعطي دائما promise في حالة fulfilled
Promise.reject(...) التي تعطي دائما promise في حالة rejected

مثال على استخدام Promise.resolve(...)

Promise.resolve('Success')
  .then(function (result) {
    console.log(result); // Success
  })
  .catch(function (error) {
    console.log(error);
  });

أول then ستم تنفيذها فورًا دون أي عقبات لأنها مرتبطة بـ Promise.resolve('Success') بشكل مباشر فطالما حصل resolve فسيتم تنفيذ أول then فورًا

والـ catch سيتم تنفيذها إذا حصل أي throw أو Promise.reject كما وضحنا

مثال على استخدام Promise.reject(...)

Promise.reject('Error')
  .then(function (result) {
    console.log(result);
  })
  .catch(function (error) {
    console.log(error); // Error
  });

في حالة Promise.reject دائما وابدًا سيتم تنفيذ الـ catch
وسيتم تجاهل كل الـ then

.finally(...)

هذه الدالة تكتب في نهاية السلسة بعد الـ .catch(...)

Promise
  .then(...)
  .then(...)
  .then(...)
  .catch(...)
  .finally(...)

وتنفذ دائمًا حتى اذا حصل fulfilled أو rejected
أي تنفذ في نهاية العملية سواء نجحت أو فشلت

تستخدم غالبًا عندما تكون متصل بـ server أو بـ database وتريد القيام بعملية ما
ثم بعد ما يتم تنفيذ العملية سواء نجحت أو فشلت نقوم بقطع الاتصال بهذا الـ Server

Promise
  .then(...) // Connecting to Database
  .then(...) // Some operation in Database
  .then(...) // Something went wrong in Database
  .then(...) // Print the result
  .catch(...) // Print the error
  .finally(...) // Disconnecting from Database (for example)

تحويل callback إلى promise

هذا سيكون أخر فقرة في مقالتنا البسيطة
إن شاء الله تكون استوعبت المفهوم العام للـ promise وتطبيقاته

الآن لنفترض أننا لدينا دالة تدعى add تقوم بأخذ أراي ورقم ما ثم تجمع هذه الرقم على جميع عناصرها

function add(arr, value, fun) {
  let newArr = [...arr];
  for (let i = 0; i < arr.length; i += 1) {
    newArr[i] += value; // add by value to each element
  }
  fun(newArr); // call callback function with newArr argument
}

مثل الدالة في مقالة الـ callback
غالب الأمر ستجد أننا نفضل استخدام الـ promise بدلًا من الـ callback
لذا ستجد نفسك تحول دوال الـ callback الى دوال promise

وهذا له فائدة اخرى سنعرفها في درس الـ await، حيث أننا نستطيع استخدام الـ await مع الـ promise لكن هذا سنشرحه في مقالة اخرى

دالة الـ add حاليًا نستدعيها بهذا الشكل

let number = [1, 2, 3, 4, 5];

add(number, 5, function (result) {
  console.log(result); // OUTPUT: [6, 7, 8, 9, 10]
});

التحويل من callback إلى promise أسهل ما يمكن
أولًا نقوم بعمل دالة أخرى والتي ستكون نسخة الـ promise

function addPromise(arr, value) {
  return new Promise(function (resolve, reject) {});
}

الدالة ستستقبل نفس المتغيرات التي تستقبلها دالة الـ callback الـ arr والـ value
وستقوم بإرجاع promise لأن هذا ما نريده

الفكرة هي استدعاء الدالة الأصلية add داخل هذا الـ promise
ثم عمل resolve لناتج الـ callback

function addPromise(arr, value) {
  return new Promise(function (resolve, reject) {
    add(arr, value, function (newArr) {
      resolve(newArr); // send the newArr in the resolve
    });
  });
}

كل ما فعلناه هو عندما يتم استدعاء الـ callback الخاص بدالة الـ add الأصلية قمنا بعمل resolve للناتج

هكذا نستطيع استخدامها كـ promise وننتظر الناتج في الـ then

addPromise(number, 5).then(function (result) {
  console.log(result); // OUTPUT: [6, 7, 8, 9, 10]
});

طبعًا نستطيع عمل reject داخل نسخة الـ promise بأي شرط

مثلًا لا نريد أن نجمع أرقامًا سالبة

function addPromise(arr, value) {
  return new Promise(function (resolve, reject) {
    add(arr, value, function (newArr) {
      if (value < 0) reject("Can't add negative number.");

      resolve(newArr); // send the newArr in the resolve
    });
  });
}

حينها نستخدم الـ catch ليتعامل مع الأمر

addPromise(number, 5)
  .then(function (result) {
    console.log(result); // OUTPUT: [6, 7, 8, 9, 10]
  })
  .catch(function (err) {
    console.log(err); // ERROR: Can't add negative number.
  });

مثال عملي باستخدام دالة fetch

في الجافاسكريبت لدينا دالة الـ fetch
التي تستخدم للتعامل مع الـ API واستخراج البيانات من الملفات وغيرها

fetch(URL); // return a promise

فالـ fetch بسيطة جدًا فقط تأخذ المسار وترج لك promise
بالتالي ان حصل resolve للناتج فسيتم تنفيذ then وان حصل اي reject فسيتم تنفيذ الـ catch

نحن من أجل الشرح لنستخدمها لاستخراج البيانات من ملف JSON لذا سننشيء ملف JSON ولنسميه file.json

دعونا نأخذ مثال لبيانات منتج معين وليكن نوع من انواع الهواتف الذكية العالمية
نفتح ملف file.json ثم نكتب البيانات بهذا الشكل

{
  "name": "Redmi 9",
  "brand": "Xiaomi",
  "colors-available": ["gray", "violet", "green"],
  "ram": 4,
  "space": 64,
  "fast-charging": true,
  "water-resistant": false
}

أفترض أنك تعرف ما هو الـ JSON من الأساس, وإن كنت لا تعرف فأرجوا أن تبحث قليلًا وتأخذ فكرة عنه، إنه وسيلة متعارف عليها لتخزين البيانات، وأظن أنك بمجرد قراءتك للملف فهمته

دعونا نبدأ بكود بسيط لاستخراج البيانات من ملف file.json

fetch('./file.json').then(function (res) {
  console.log(res); // response is a Response object
});

هنا قيمة الـ res الآن ليست البيانات التي بداخل ملف الـ file.json
قيمة الـ res هي Response object

ما هو الـ Response object ؟

الأمر ببساطة لأن طبيعة دالة الـ fetch هي التعامل مع API وروابط مختلفة خارجية
وهذه الروابط قد تجلب لنا بيانات متنوعة ومختلفة مثلًا نص او json أو حتى صورًا

لذا لدينا object من الـ Response يكون وسيطًا ليعطينا معلومات عن ما تم احضاره وكيف نتعامل معه اذا كان نصًا او صورة على سبيل المثال

داخل object الـ Response الذي في حالتنا اسميناه res
لدينا دوال و متغيرات متنوعة منها

ملحوظة: كل هذا الكلام لا يمد بالـ Promise بصلة لكننا نقوله لان دالة الـ fetch تستخدم هذا الـ Response object لوصف ومعرفة معلومات عن حالة هذا الـ response
لذا اضطرينا إلى شرح بعض معلومات عن الـ Response object

يمكنك قراءة المزيد من المعلومات عن الـ Response object من هنا

تحويل الـ JSON إلى Javascript Object

لا نريد أن نخوض في الأمر كثيرًا، لكن ما يهمنا هنا هي دالة res.json() وهي ترجع لنا promise تقوم بتحويل البيانات من JSON إلى Javascript object

fetch('./file.json').then(function (res) {
  return res.json(); // return a resolved promise that parsing json to javascript object.
});

دالة الـ then الآن سترجع res.json()
الذي بدوره سيرجع لنا promise معمول له resolve بالبيانات التي بداخل الـ Response object بعد تحويلها إلى javascript object

طالما الـ then الأولى أرجعت لما promise فسنحتاج لـ then أخرى

fetch('./file.json')
  .then(function (res) {
    return res.json();
  })
  .then(function (data) {
    console.log(data);
  });

الآن قيمة data ستكون البيانات كـ json

{
  "name": "Redmi 9",
  "brand": "Xiaomi",
  "colors-available": [
    "gray",
    "violet",
    "green"
  ],
  "ram": 4,
  "space": 64,
  "fast-charging": true,
  "water-resistant": false
}

أول then كانت تقوم بتحويل البيانات داخل الـ Response إلى Promise يحتوي على البيانات كـ Javascript object
وثاني then استقبلت الـ Javascript object العائد من الـ then الاولى ثم قامت بطباعته

طبعًا يمكنك استخدام الـ catch في حالة حدود أي reject

fetch('./file.json')
  .then(function (res) {
    return res.json();
  })
  .then(function (data) {
    console.log(data);
  })
  .catch(function (error) {
    console.log(error);
  });

حسنًا يكفي هذا القدر عن الـ promise سنتكلم عنه مجددًا وعن الـ Promise.all بشيء من التفصيل لكن في مقالة أخرى
المقالة التالية ستكون عن الـ async/await وهي طريقة جديدة وبسيطة للتعامل مع الـ promise أو الدوال التي تستغرق وقتًا بشكل عام