جحيم الـ Callback

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

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

المقدمة

سنشرح في هذا الدرس مفهوم الـ callback في الجافاسكريبت والأمور التي تدور حوله ومشاكله

ما هو الـ callback function ؟

أولًا يجب أن نعرف أن الـ callback function هي دالة تقوم باستقبال دالة أخرى كـ argument ثم يتم استدعاءها في أي مكان داخل الدالة

الأمر ليس بأن تستدعي الدالة داخل الـ argument، بل أن ترسل الدالة بذاتها كـ argument لدالة أخرى، ما الفرق ؟

النظرة الخاطئة عن الـ callback

حسنا انتبه في المثال التالي

function sum(x, y) {
  return x + y;
}

function print(s) {
  console.log(s);
}

هنا لدينا دالة تدعى sum تقوم بارجاع مجموع الرقمين ودالة تدعى print تقوم باستقبال قيمة وتطبعها

عندما تستقبل دالة print دالة sum كهذا

print(sum(5, 10)); // OUTPUT: 15

هل هذا يسمى callback ؟ الاجابة بكل بساطة لا
لان دالة print لم تستقبل دالة sum بل هي فقط استقبلت ناتج دالة sum بمعنى انها استقبلت رقم وليس دالة

المفهوم الصحيح لدالة الـ callback

الـ callback هو أن ترسل الدالة نفسها وليس أن ترسل ناتج استدعاءها
إن أرسلت الدالة نفسها فيمكنك أن تستدعيها داخل الدالة

بمعنى أننا سنرسل اسم الدالة كهذا

print(sum);

نحن هنا أرسلنا الدالة نفسها دون أن نستدعيها بـ ()

ونستطيع أن نستدعيها داخل دالة الـ print كهذا

function sum(x, y) {
  return x + y;
}

function print(s) {
  let result = s(5, 10); // call the function that we passed
  console.log(result);
}

print(sum); // return 15

هنا عندما أرسلنا دالة sum إلى دالة print دون استدعاءها، بهذا الشكل print(sum) أرسلنا فقط اسم الدالة دون () كما تلاحظ
اصبح الآن معنا reference داخل دالة الـ print يمثل دالة الـ sum
وكل ما فعلناه داخل الـ print هو أننا استدعينا دالة s التي تمثل دالة الـ sum بالطبع
هنا الـ s تحتاج لقيمتين عند استدعاءها
لان الـ sum كانت تستقبل قيمتين عند استدعاءه
لذا ارسلنا للـ s قيمتين الـ x, y هكذا s(x, y) كما يريد

بعد ما استدعينا s(x, y) وارسلنا القيم سيتم تنفيذ محتوى دالة الـ sum
وهي جمع القيمتين return x + y; ثم استقبلنا الناتج في متغير result هكذا let result = s(5, 10); وطبعناه console.log(result);

لاحظ أننا استدعينا دالة الـ s التي نفذت محتوى دالة sum
لانه كما قلنا دالة s اصبحت reference لدالة الـ sum

وقمنا بإرسال أي قيم ثم طبعنا الناتج

الشاهد من المثال أننا قمنا بإرسال دالة واستدعيناها في أي مكان داخل الدالة

ملحوظة: عليك أن تنتبه إلى عدد المتغيرات التي يقبلها الـ callback
بمعنى أننا لو افترضنا أن الـ sum تستقبل 3 متغيرات فقط x, y, z والـ callback يستقبل 2 فقط كما رأينا هنا s(x, y) يمكن أن يكون لديك callback يستقبل متغير واحد فقط أو 5 مثلًا أو لا يستقبل شيء فجيب أن تعرف طبيعة عمل الـ callback الذي أمامك

أمثلة على الـ callback

سنستعرض بضع أمثلة لتوضيح الأمر أكثر

المثال الأول

function sum(x, y, fun) {
  let result = x + y;
  return fun(result);
}

function multipleBy5(n) {
  return n * 5;
}

let result = sum(5, 10, multipleBy5);
console.log(result);

لناخذ هذا المثال خطوة خطوة
لدينا دالة تدعى sum تستقبل قيمتين x, y وتستقبل callback يدعى fun وكل ما تفعله الدالة هو جمع القيمتين في متغير result
ثم نستدعى دالة fun التي تستقبل المجموع return fun(result); ثم نرجع القيمة الراجعة من fun

نلاحظ هنا أن الـ fun هي callback function تحتاج لاستقبال متغير واحد ويبدوا أنها تقوم بإرجاع قيمة ما

لذا نحتاج لدالة تستقبل متغير وترجع قيمة
وهذا ما فعلناه مع دالة multipleBy5 التي تستقبل قيمة عددية ثم ترجعه مضروبًا في 5

هل يمكنك أن تأخذ 5 دقائق وتحلل هذا السطر const result = sum(5, 10, multipleBy5); بالمعطيات التي أخرجانها للتو ؟

هذا اختبار صغير يمكنك أن تتوقف عن القراءة وتفكر بتتبع المثال الذي بالأعلى ومحاولة فهمه بنفسك ثم أرجع وأكمل القراءة

هنا ببساطة sum(5, 10, multipleBy5); قمنا بإعطاء قيم 5, 10 إلى x, y ثم أرسلنا multipleBy5 لتكون دالة callback سندخل الآن داخل دالة الـ sum لنرى ماذا سيحدث
let result = x + y; أول شيء يقوم بجمع القيم وتخزينها في result
قيمة result الآن قيمتها 15
ثم نستدعي fun التي في حالتنا الآن أصبحت reference لدالة الـ multipleBy5 ثم أرسلنا قيمة الجمع لها fun(result)
الآن تم استدعاء دالة multipleBy5 عن طريق fun الـ reference الخاص بها
دالة multipleBy5 ستستقبل قيمة المجموع ثم تضربه في 5 وترجع القيمة return n * 5;
لذا القيمة الراجعة الآن من multipleBy5 هي 15 * 5 = 75
لذا نعود لدالة الـ sum، والآن هنا return fun(result); القيمة الراجعة من fun ستكون 75
وستقوم دالة الـ sum بإرجاع القيمة لتكون قيمة الـ result هنا let result = sum(5, 10, multipleBy5); تساوي 75

الأمر قد يكون يحتوي على لف ودوران قليلًا لكن بالتتبع والاعتياد سيكون الأمر مع الوقت عاديًا بالنسبة لك مثل سائر الأمور

أريدك أن تستوعب طريقة استخدامنا للـ callback في المثال السابق فقط

المثال الثاني

function sum(x, y) {
  return x + y;
}

function applyOperation(x, y, fun) {
  let result = fun(x, y); // call the function that we pass it
  return result;
}

// we pass sum as function, and didn't call it
let result = applyOperation(5, 10, sum); // return 15
console.log(result); // OUTPUT: 15

هنا نفس المثال السابق تمامًا لكن هنا لدينا applyOperation(x, y, fun) تستقبل القيم x ,y التي ستُرسل للدالة fun التي ستستقبلها كذلك
ثم تستدعي الدالة هكذا fun(x, y) وترسل القيم لها ثم نرجع الناتج ببساطة

المثال بسيط لكن كيف نستفيد من هذا

الموضوع يحتاج لتوسيع نطاق تفكيرك لا أكثر
لاحظ في هذا السطر let result = fun(x, y); دالة الـ fun كانت تمثل دالة sum التي أرسلناها ودالة الـ sum كانت تستقبل قيمتين لذا دالة fun أصبحت كذلك
هنا الـ fun كانت نسخة طبق الأصل للـ sum التي أرسلناها
وستكون نسخة طبق الأصل لأي دالة سنرسلها، فالأمر لا يختصر على sum

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

أنظر هنا

function sum(x, y) {
  return x + y;
}

function sub(x, y) {
  return x - y;
}

function mul(x, y) {
  return x * y;
}

function div(x, y) {
  return x / y;
}

هنا لدينا أكثر من دالة، يمكننا أن تتوقع ما الذي سنفعله الآن
نستطيع هنا أن نرسل أي دالة منهم كـ callback لدالة الـ applyOperation

function applyOperation(x, y, fun) {
  let result = fun(x, y); // call the function that we pass it
  return result;
}

// we pass multiple functions to the callback
applyOperation(5, 10, sum); // return 15
applyOperation(5, 10, sub); // return -5
applyOperation(5, 10, mul); // return 50
applyOperation(5, 10, div); // return 0.5

هنا كما نلاحظ دالة applyOperation كل مرة تستقبل دالة callback مختلفة

أظنك الآن فهمت الفكرة العامة للـ callback إن شاء الله

إنشاء الدالة داخل الـ argument ؟

هنا في ميزة جميلة جدًا يمكننا عملها مع الـ callback وهو بدلًا من أن ننشيء دالة ثم نرسلها كـ callback
نستطيع إنشاء الدالة كـ argument بشكل مباشر، ما معنى هذا الكلام ؟

تتذكرون المثال السابق هذا

function sum(x, y) {
  return x + y;
}

function applyOperation(x, y, fun) {
  let result = fun(x, y);
  return result;
}

let result = applyOperation(5, 10, sum);
console.log(result); // OUTPUT: 15

تأملوا فيه جيدًا وقارنوه مع ما سنكتبه الآن

function applyOperation(x, y, fun) {
  let result = fun(x, y);
  return result;
}

let result = applyOperation(5, 10, function (x, y) {
  return x + y;
});
console.log(result); // OUTPUT: 15

لاحظ أننا بدل من أن ننشيء دالة sum خارج الـ applyOperation ثم نرسلها للدالة كما في المثال الأول
ما قمنا به أننا أنشأنا الدالة نفسها أثناء استدعائنا لدالة applyOperation

وهى تسمى هنا Anonymous Functions أي دالة بلا إسم، وإن فكرت في الأمر فستجد إن اسم الدالة لن يكون له فائدة هنا
لانها ستستقبل كـ reference داخل دالة الـ applyOperation كـ fun
ولن تحتاج لاسم الدالة لأنك لن تستطيع استدعاءها خارج الدالة على كل حال

ويمكنك استخدام أي طريقة تناسب الموقف الذي أنت فيه
إن كنت تريد دالة تستدعيها مرة واحدة فقط كـ callback ولن تستخدمها مجددًا فاجعلها anonymous function
وإن كنت تريد أن تستخدم الدالة في أماكن أخرى فيما بعد فاستخدم الطريقة الأولى بالطبع

مثال لدالة built-in تستخدم callback

دعونا نستكشف دالة built-in تستخدم callback بغرض شرح بعض الأمور حول الـ callback

الدالة التي أريد أن اتكلم عنها هى setTimeout هي دالة فكرتها بسيطة setTimeout(callback, timeInMilliseconds)
فقط تعطيها callback ثم تعطيها الوقت الذي سيتم استدعاء الـ callback فيه
هذا الوقت يكون ميلي ثانية والأف ميلي ثانية يساوي واحد ثانية

تأمل في الكود التالي

console.log('Starting');
setTimeout(function () {
  console.log('This will call after 1 sec');
}, 1000); // 1000 ms = 1 sec

إن فهمت فكرة setTimeout فستعرف انه سيتم طباعة starting ثم سينتظر ثانية واحدة ليتم طباعة This will call after 1 sec
لأن الـ callback تم استدعاءه بعد ثانية واحدة

محاكاة عمل setTimeout

أريدك أن تأخذ تحدي بسيط وهو محاولة تخيلية لكيف تم إنشاء دالة setTimeout
ومحاولة عمل واحدة بأنفسنا

هذا اختبار آخر يمكنك أن تتوقف عن القراءة وتفكر بعمل دالة setTimeout خاصة بك لمحاولة فهمت طريقة عمل الـ callback ثم أرجع وأكمل القراءة

حسنًا سأفترض أنك انتهيت الآن، لا تقلق إن لم تستطيع نسخ الدالة وعمل دالة مشابه للـ setTimeout فلا بأس
فنحن هنا أتينا لنتعلم لذا سأريك الدالة التي قمت بصنعها الآن

الفكرة الأساسية هنا أننا نحتاج لشيء يحدد لنا الوقت لنعرف متى سنستدعي دالة الـ callback لانه سيتم استدعائها بعد مدة من الزمن كما نعرف
لذا استخدمنا Date.now() وهي دالة في الجافاسكريبت داخل الـ Date تقوم بإرجاع الوقت الحالي بالميلي ثانية

وهذا ما نريده، الآن لنتأمل الكود التالي ونحلله

console.log('starting');

function ourSetTimeout(fun, ms) {
  let stopAt = Date.now() + ms;
  while (Date.now() < stopAt) continue;
  fun();
}

ourSetTimeout(function () {
  console.log('done');
}, 1000);

أنشأنا الدالة الخاصة بنا ourSetTimeout(fun, ms)
تستقبل fun التي ستكون دالة الـ callback ثم ms والذي سيكون الوقت بالميلي ثانية
داخل الدالة في السطر التالي let stopAt = Date.now() + ms; قمنا بإنشاء متغير سميناه stopAt
الذي سيكون كما يوحي الإسم نهاية الوقت أي الوقت الذي سننتهي عنده وهو الوقت الحالي + الوقت الذي حددناه بالميلي ثانية
ثم لدينا while (Date.now() < stopAt) continue; وهو إذا ترجمته بشكل حرفي ستفهمه وهو طالما الوقت الحالي أقل من الوقت الذي سننتهي عنده .. استمر
ثم في النهاية عندما تنتهي الـ while يكون الوقت الذي حددناه وصل لنهايته
لذا عند هذه النقطة استدعي الـ callback الذي أرسلناه للدالة fun()
لنطبع في النهاية كلمة done

Callback Hell

ما هو الـ Callback Hell ؟
حسنًا المعنى الحرفي هنا هو جحيم الـ callback وهو شيء نحاول دائما وأبدًا أن نتجنبه لأنها تجلب لنا المشقة والتعب والصعوبة في قراءة وتعديل الكود والتعقيد وتشابك الأكواد والأمر يصبح حرفيًا كالجحيم

حسنا كيف يبدوا لكي نتجنبه ؟

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

سنستخدم دالة setTimeout كمثال توضيحي هنا

تتذكرون دالة setTimeout ؟ ألقو نظرة عليها مجددًا

console.log('Starting');
setTimeout(function () {
  console.log('This will call after 1 sec');
}, 1000);

لدينا طلب هنا وهو أن نقوم بطباعة الأعداد من 1 إلى 10 بشرط أن الفرق الزمني لطباعة الأعداد ثانية واحدة على سبيل المثال

حسنًا الأمر قد يكون سهل لنرى الأمر على رقم 1 و2 كبداية

setTimeout(function () {
  console.log('No. 1');
  setTimeout(function () {
    console.log('No. 2');
  }, 1000);
}, 1000);

هنا سيتم طباعة No. 1 بعد ثانية ثم سيتم طباعة No. 2 بعد الثانية التالية
لانه كما تلاحظ لدينا nested setTimeout function بمعنى أن لدينا دالة setTimeout داخل setTimeout دالة داخل دالة
لأننا كما قلنا نحتاج أن يكون الفارق الزمني واحد ثانية بين كل رقم
لهذا جعلنا دالة setTimeout الثانية بداخل الدالة الأولى
بحيث عند انتهاء الدالة الأولى تبدأ دالة setTimeout الثانية بالعمل وتنتظر ثانية أخرى

الآن لكي نطبع No. 3 بعد No. 2 نحتاج لأن نجعل دالة الـ setTimeout الخاصة بالرقم 3 تبدأ داخل دالة الـ setTimeout الخاصة بارقم 2

بهذا الشكل

setTimeout(function () {
  console.log('No. 1');
  setTimeout(function () {
    console.log('No. 2');
    setTimeout(function () {
      console.log('No. 3');
    }, 1000);
  }, 1000);
}, 1000);

حسنًا كيف سيكون شكل الكود عندما نصل للرقم 10 ؟ دعونا نرى

setTimeout(function () {
  console.log('No. 1');
  setTimeout(function () {
    console.log('No. 2');
    setTimeout(function () {
      console.log('No. 3');
      setTimeout(function () {
        console.log('No. 4');
        setTimeout(function () {
          console.log('No. 5');
          setTimeout(function () {
            console.log('No. 6');
            setTimeout(function () {
              console.log('No. 7');
              setTimeout(function () {
                console.log('No. 8');
                setTimeout(function () {
                  console.log('No. 9');
                  setTimeout(function () {
                    console.log('No. 10');
                  }, 1000);
                }, 1000);
              }, 1000);
            }, 1000);
          }, 1000);
        }, 1000);
      }, 1000);
    }, 1000);
  }, 1000);
}, 1000);

أظنك بدأت تخاف قليلًا الآن!؟
هذا يا صديقي ما يسمى بالـ Callback Hell
هل ترى هذا الفراغ الشاسع في ناحية اليسار الذي يشبه المثلث ؟
إن رأيته فأنت ترسم طريقك نحو جحيم الـ callback وعليك أن تتوقف وتفكر بأسلوب أخر لكتابة الكود

ما مدى الخطورة ؟

المثال السابق كان مجرد كود يقوم بطباعة الأعداد من 1 لـ 10
فكر في نفس هذا الشكل لكن ما كود معقد أكثر

فمثلًا

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

مع تعقيد الكود أكثر كيف سيبدو الـ callback hell هنا ؟ سيكون جحيمًا حقيقيًا لك، فقط تخيل !

هل استوعبت المشكلة الآن !؟

كيف نحل المشكلة إذًا ؟

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

ملحوظة: يوجد أكثر من حل، وكل حل يختلف من كود لكود ومن شخص لشخص بحسب الحالة
أنا هنا غرضي توضيح الفكرة العامة باستعراض بعض الحلول للمشكلة لا أكثر

فعلى سبيل المثال، لنحاول في الكود الذي يطبع الأعداد من 1 لـ 10 تجنب الـ callback hell

المشكلة بسيطة جدًا ومباشرة لأنك تعرف ما تريده وتعرف الفترة الزمنية
تعرف أنهم 10 أعداد الفاصل الزمني ما بينهم واحد ثانية

لذا يمكنك عمل for loop من 1 لـ 10 ثم تطبع العدد الأول بعد ثانية والعدد التاني بعد ثانيتين والعدد الثالث بعد ثلاث ثواني ... وهكذا

function logMessageAfter(message, ms) {
  setTimeout(function () {
    console.log(message);
  }, ms);
}

for (let i = 1; i <= 10; i += 1) {
  logMessageAfter(`No. ${i}`, i * 1000); // i * 1000 ms = i * 1 sec
}

أنشأنا دالة logMessageAfter تاخذ الرسالة والمدة الزمية
داخلها يوجد setTimeout تطبع الرسالة بعد المدة الزمنية

ثم لدينا loop تقوم باستدعاء الدالة في كل مرة مع زيادة المدة الزمنية في كل لفة ثانية واحدة i * 1000 في اللفة الاولى ستكون ثانية واللفة التالية ستكون ثانيتين وهكذا لان الـ i تزيد قيمتها في كل مرة

لكن بالطبع حل المثال السابق كان بسيط ومباشر لأنك تقوم بعملية تكرارية
لكن في الحياة الواقعية والمشاريع الحقيقية يكون الأمر أعقد بكثير
المثال السابق كان غرضه التوضيح من أجل الشرح لا أكثر

الشكل الشائع للـ callback hell

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

لنستعرض مثالًا أخيرًا نختم به المقالة
لدينا مجموعة من الأرقام في أراي تدعى numbers

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

ولدينا دالة تدعى add تقوم بأخذ arr و value و fun

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
}

كل ما نقوم به هى أننا ننشيء متغير جديد يدعى newArr يكون نسخة للـ arr
let newArr = [...arr]; لكي لا نعدل على قيم الأراي الأصلية، ثم نمر على كل عناصر الـ newArr ونجمع عليهم قيمة الـ value هكذا newArr[i] += value;
ثم نستدعى دالة الـ callback وهي الـ fun(newArr) ونرسل لها الأراي الجديدة بعد التعديل

لنرى كيف نستدعي ونستخدم دالة الـ add

add(numbers, 10, function (newArr) {
  console.log(newArr); // OUTPUT: [11, 12, 13, 14, 15];
});

console.log(numbers); // OUTPUT: [1, 2, 3, 4, 5]

قمنا بإرسال numbers و 10 بالتالي أولًا ستُنشيء الدالة نسخة عن الـ numbers وهى newArr ثم ستمر الدالة على كل عناصر الـ newArr وتضيف 10 عليهم

ثم في النهاية سيتم استدعاء الـ callback التي أرسلناها والتي تقوم باستقبال الـ newArr ثم ستقوم فقط بطباعته

طبعًا قيمة الـ numbers الأصلية لم تتغير لاننا نسخناها وتعاملنا مع النسخة وليس مع الأصل

حسنًا لنعلوا بالمستوى قليلًا ونقول أننا بعد ما جمعنا 10 على كل عنصر نريد أن نضربهم في 2

ولنفترض أن هناك دالة تشبه add لكن تدعى mul تقوم بعملية الضرب بدلًا من الجمع

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

بالتالي إن اردنا جمع 10 على العناصر ثم ضربهم في 2 فسيكون الكود هكذا

add(numbers, 10, function (newArr) {
  mul(newArr, 2, function (newArr2) {
    console.log(newArr2); // OUTPUT: [22, 24, 26, 28, 30];
  });
});

console.log(numbers); // OUTPUT: [1, 2, 3, 4, 5]

هكذا بعد ما قمنا بجمع 10 على العناصر أخذنا newArr في الـ
callback ثم قمنا باستدعاء دالة mul لكي نضرب كل عناصرها في 2 كما اتفقنا
وmul عندما تنتهي ستستدعي الـ callback خاصتنا والتي ستستقبل
newArr2 التي ستكون الأراي النهائية بعد عملية الضرب ثم طبعناها كما ترى

حسنًا لنعلوا بالمستوى مجددًا ونقول أننا نريد عمل عمليات متعددة مثل الجمع والضرب والطرح والقسمة ولنفترض أننا لدينا دالة sub للطرح ودالة div للقسمة
بنفس فكرة الدوال السابقة هكذا

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

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

الآن نريد عمل سلسلة من العمليات على الأراي كالآتي، جمع 10 عليهم ثم ضربهم في 2 ثم طرح 5 منهم ثم قسمهم على 2 ثم جمع عليهم 5 ثم ضربهم في 4

add(numbers, 10, function (newArr) {
  mul(newArr, 2, function (newArr2) {
    sub(newArr2, 5, function (newArr3) {
      div(newArr3, 2, function (newArr4) {
        add(newArr4, 5, function (newArr5) {
          mul(newArr5, 4, function (finalArr) {
            console.log(finalArr);
          });
        });
      });
    });
  });
});

أريدك أن ترحب مجددًا بصديقنا العزيز الأستاذ Callback Hell !!

هذا هو الشكل الشائع له وهو أن كل callback يعتمد بشكل أساسي على الـ callback الذي قبله

مناقشة المشكلة الشائعة

هل يمكنك القاء نظرة على الكود السابق وتستخرج منه العيوب ؟

ستجد أنه صعب القراءة فلو أحضرت صديقك المبرمج وقلت له أن يفهم الكود فلن يفهم الكود إلا بصعوبة شديدة
وأنت بنفسك إن رجعت للكود بعد ساعة فستجد أنك ارتبكت ولن تفهمه بسرعة

أيضًا بالتالي مع صعوبة الفهم فلن تستطيع تعديل الكود بسهولة
لانه كما قلت صعب القراءة والتتبع وكل جزء يعتمد على الذي قبله
بالتالي إن عدلت جزء فستضطر لتعديل العديد من الأجزاء هنا وهناك

مع العلم أن مثالنا صغير غرضه التوضيع والتبسيط، فتخيل مع كود حقيقي في مشروع واقعي سيكون معقد ومتشابك وأي تعديل ستضطر لكتابة الكود من البداية في أغلب الأوقات

كما قلت لا يوجد حل وحيد ينفع لجميع الحالات
بمعنى أن الكود السابق قد نحله بالعديد من الطرق
لكن إن أحضرت كودًا أخر به callback hell فسنحله بطرق أخرى مختلفة تمامًا

let newNumbers = [];
add(numbers, 10, function (newArr) {
  newNumbers = newArr;
});
mul(numbers, 2, function (newArr) {
  newNumbers = newArr;
});
sub(numbers, 5, function (newArr) {
  newNumbers = newArr;
});
div(numbers, 2, function (newArr) {
  newNumbers = newArr;
});
add(numbers, 5, function (newArr) {
  newNumbers = newArr;
});
mul(numbers, 4, function (newArr) {
  newNumbers = newArr;
});

console.log(newNumbers); // OUTPUT: [4, 8, 12, 16, 20]
console.log(numbers); // OUTPUT: [1, 2, 3, 4, 5]

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

لا يوجد حل ثابت يمكنني أن اخبرك به سوى أن تحاول أن تتجنب الخوض في الـ callback hell على قد استطاعتك بسبب عيوبه

الـ promise والـ async/await

في النهاية لدينا مفهومان ظهراه مؤخرًا وهما الـ promise والـ async/await الذي يجمعهما فكرة العمليات الغير متزامنة Asynchronous

وتسببا في حل مشاكل متنوعة من ضمنها الـ callback hell
لكن في مقالات منفصلة سنتكلم على كل واحد منهما بالتفصيل إن شاء الله