الـ async-await بديل الـ Promise ؟
السلام عليكم ورحمة الله وبركاته
الفهرس
المقدمة
سنتكلم في هذه المقالة عن الـ async/await
وهو طريقة جديدة للتعامل مع الاكواد التي قد تستغرق وقتًا طويلًا
ويعد الطريقة الأكثر شيوعًا حاليًا لبساطتها وسهولتها
قبل أن نكمل عليك أولًا ان تقرأ المقالات السابقة التي تكلمنا فيها عن الـ الـ Promise، وعود الجافاسكريبت ! والـ جحيم الـ Callback
هذه المقالة سنتكلم عن مفاهيم مهمة في عالم البرمجة لان مفهوم الـ asynchronous function
أو الدالة الغير متزامنة هو مفهوم مهم جدًا وشائع في عالم البرمجة
لذا سأحاول أن اعطيه حقه هنا
لكن أولًا
ما هي الـ async/await ؟
هذه مجرد keywords
ستجدها في العديد من اللغات
تستخدم مع الـ Asynchronous Function
ويطلق عليها ايضًا Non-Blocking Function
دالة غير مُوقِفَة
ما معنى هذه المصطلحات من الأساس ؟
لنتكلم أولًا عن الفرق بين الـ Synchronous
و الـ Asynchronous
Synchronous
لنبدأ مع المفهوم المألوف لدينا وهو الـ Synchronous
وهو ببساطة أن كل سطر كود يتم تنفيذه بالترتيب
بمعنى أنه لا يمكن أن تبدأ الخطوة الثانية قبل أن تنهي الخطوة الأولى
لنتخيل أن لدينا ثلاث دوال بسيطة بهذا الشكل
function f1() {
console.log('f1 done');
}
function f2() {
console.log('f2 done');
}
function f3() {
console.log('f3 done');
}
ثم قمنا باستدعاء الدوال بهذا الشكل
f1();
f2();
f3();
ناتج الطباعة سيكون بالطبع هكذا
f1 done
f2 done
f3 done
لاننا استدعينا الدالة f1
ثم f2
ثم f3
بهذا الترتيب
مستحيل أن يتم تنفيذ ما بداخل الدالة f2
قبل أن تنتهي الدالة f1
من تنفيذ ما بداخلها
فهذه تسمى عملية متزامنة كل دالة تنتظر أن تنتهي الدالة أو الكود الذي قبلها لتبدأ هي التنفيذ
بمعنى آخر عندما يصل البرنامج لتنفيذ الدالة f1
فأنه سيتوقف عن تنفيذ باقي الأسطر وباقي الدوال لحين الانتهاء من تنفيذ الدالة f1
وهذا يسمى Blocking Function
بمعنى أن الدالة f1
أوقفت كل البرنامج لحين انتهاءها
وكذلك الأمر مع f2
و f3
وأي كود برمجي بشكل العام لأن الأكواد يتم تنفيذها بالترتيب أي synchronous
هل هذا الأمر مهم أن نفهمه أو له عواقب ؟، بالطبع نعم!
قبل أن نكمل دعونا نعرف الوقت المستغرق للانتهاء من تنفيذ الدوال السابقة
let start = Date.now();
function f1() {
console.log('f1 done at :', Date.now() - start);
}
function f2() {
console.log('f2 done at :', Date.now() - start);
}
function f3() {
console.log('f3 done at :', Date.now() - start);
}
هنا نحن فقط نحسب الوقت التي ستستغرقه كل دالة للتنفيذ
عندما نستدعي الدوال سيكون ناتج الطباعة هكذا
f1 done at : 0
f2 done at : 0
f3 done at : 0
استغرق تنفيذ جميع الدوال 0
ميلي ثانية بسبب أنها دوال تقوم بعملية طباعة لا أكثر
ملحوظة
: الأرقام التي ستراها ليست ثابتة بل تقريبية فعندما تنفذ الدوال أكثر من مرة ستجد أن الرقم يتغير أحيانًا يكون0
أو1
أو حتى5
لاختلاف سرعة الجهاز أو المتصفح ستجد فروقات صغيرة جدًا في كل مرة
مشكلة الـ Synchronous
تخيل معي أن الدالة f1
تقوم بعملية ثقيلة وتستغرق وقتًا طويلًا
مهما كانت تلك العملية سواء إحضار معلومات من API
او تقوم بأي اتصال بـ Server
ما
لنحاكي المشكلة بمثال بسيط
let start = Date.now();
function f1() {
let i = 0;
// count to 1 billion
while (i < 1000000000) {
i += 1;
}
console.log('f1 done at :', Date.now() - start);
}
function f2() {
console.log('f2 done at :', Date.now() - start);
}
function f3() {
console.log('f3 done at :', Date.now() - start);
}
كما ترى هنا فـ f1
هنا تقوم بعملية ثقيلة بأنها تعد من صفر لمليار!
إن استدعينا الدوال بنفس الترتيب لنرى كم استغرق تنفيذ كل دالة
f1 done at : 1614
f2 done at : 1614
f3 done at : 1614
استغرق تنفيذ f1
بمفردها فقط 1614
ميلي ثانية أي ما يقارب ثانية ونصف!
ثم تم بدأ تنفيذ f1
و f2
وانتهوا في 1614
برغم بأنهما يستغرقان فقط 0
ميلي ثانية
لكنهما اضطرا لانتظار f1
لكي يبدأه
رأيت الفرق ؟ استوعبت المشكلة ؟
تخيل أن الدالة f1
هي دالة تقوم بعملية ثانوية مستقلة
والدالتين f2
و f3
لا تعتمدان على f1
بأي شكل من الأشكال
فلماذا تضطر الدالتان f2
و f3
لانتظار f1
ما دام أنهما مستقلين، وقد تضطر للانتظار دقائق وهذا وارد هل الدالتين f2
وf3
سينتظران كل هذا الوقت ؟
تخيل هذه المشكلة تواجهك في موقعك أو في مشروعك، بأن هناك دوال تجعل كامل التطبيق يتوقف أو يتأخر بدون سبب
لما تجعل شيئين لا يعتمدان على بعضهما بأن ينتظر أحدهما الآخر ؟ وبدون سبب!
هذه المشكلة تحدث عندما يكون الكود يسير بشكل synchronous
أي متزامن بأن كل سطر كل ينتظر السطر السابق أن ينتهي ليبدأ هو
حتى وإن كانت الأكواد لا تعتمد على بعضها البعض
Asynchronous
هنا ظهر مفهوم Asynchronous
وهو جعل جزء ما من الكود يتم تنفيذه في الخلفية دون ان يأثر في باقي أكواد التطبيق
اي ان البرنامج سيظل ينفذ باقي الأوامر والدوال الأخرى بشكل اعتيادي دون أن يتأثر أو ينتظر الجزء أو الدالة التي تحتاج وقتًا طويلًا
ببساطة نحن نعرف أن الدالة f1
ثقيلة وتستغرق وقتًا وتجعل كامل البرنامج يتوقف لحين انتهاءها
لذا بجعل الدالة f1
تكون Asynchronous
سيتم تنفيذها في الخلفية
وسيتم تنفيذ f2
و f3
على الفور بالتزامن مع تنفيذ f1
لانها سيتم تنفيذها في الخلفية
ملحوظة
: في حقيقة الأمر يوجد شيء يدعىEvent Loop
و أشياء أخرى تحدث في الخلفية لكني لن أتطرق لها هنا بل سيتم عمل مقالة أخرى سنتكلم عن ما يحدث في الخلفية وما هي الـEvent Loop
بالتفصيل وكيف تتعاملالجافاسكريبت
مع تلك الأمور
وسبب وجود الـEvent Loop
فيالجافاسكريبت
أنهاOne Thread
في اللغات الأخرى التي تدعم الـMultiple Threads
فأي عملية تكونAsynchronous
يتم إنشاءthread
خاص
دعم الـ Promise لمفهوم الـ Asynchronous
سأفترض هنا أنك قرأت المقالة الخاصة بالـ Promise
لذا سأدخل في المثال بشكل مباشر
سنفترض أن هناك بيانات نريد الحصول عليها من ملف JSON
وسنستخدم دالة fetch
function f1() {
// fetch json data from a local file
fetch('./file.json')
.then(function (res) {
// convert JSON object to Javascript object
return res.json();
})
.then(function (data) {
// console.log(data);
console.log('f1 done at :', Date.now() - start);
});
}
كما ترى هنا فالـ f1
تقوم بجلب البيانات من ملف JSON
وهي عملية قد تستغرق وقتًا غالبًا
دالة fetch
تقوم بإرجاع promise
والـ promise
هنا يتبع نهج الـ asynchronous
بمعنى أي دالة أو عملية promise
يتم تنفيذها في الخلفية ويكمل البرنامج تنفيذ باقي الأكواد لحين انتهاءها
وعندما تنتهي الـ promise
من التنفيذ يقوم البرنامج بالرجوع لها لتنفيذ الـ then
الخاص بها
الآن إن استدعينا الدوال بنفس الترتيب
f1();
f2();
f3();
لنرى كم استغرق تنفيذ كل دالة
f2 done at : 0
f3 done at : 1
f1 done at : 105
لاحظ أنه تم الانتهاء f2
و f3
أولًا و f1
كانت آخر شيء
لان الـ f1
كانت promise
لذا اعتبر البرنامج أنها دالة asynchronous
فتركها للنهاية كما قلنا
أذكر مجددًا بأن هناك تفاصيل كثيرة تدور في الكواليس بسبب كون لغة
الجافاسكريبت
لغةOne Thread
وكيف تتعامل مع الـAsynchronous
عن طريق الـEvent Loop
سنخصص مقالة نتكلم عن هذه التفاصيل
استخدام async/await بدلًا من الـ Promise
هناك طريقة جديدة للتعامل مع الـ Promise
وهي الطريقة الاكثر استخداما وتداولا
لبساطتها وتنظيمها وتكون مألوفة لنا بعيدا عن اسلوب الـ then
الكثير
وهي طريقة الـ async/await
async function f1() {
let res = await fetch('./file.json');
let data = await res.json();
// console.log(data);
console.log('f1 done at :', Date.now() - start);
}
اظن انك بمجرد قراءتك للدالة فهمت إلى حد ما ما يحصل
نحن فقط كتبنا async
قبل الدالة f1
ليتم التعرف عليها كدالة غير متزامنة Asynchronous function
let res = await fetch('./file.json');
هنا يتم استقبال الـ Response Object
الراجع من دالة الـ fetch
في متغير res
ومعنى كلمة await
انه سينتظر انتهاء دالة الـ fetch
من احضار البيانات
الـ await
لن تعمل الا في الدوال الغير متزامنة بمعنى انه ان لم نكتب async
قبل إسم الدالة فالـ await
لن تعمل
let data = await res.json();
// console.log(data);
هنا نفس الأمر سينتظر لحين الانتهاء من تحول الـ res
من JSON
لـ Javascript Object
ثم يخزن الناتج في متغير data
ثم يطبعه إن أردنا ذلك
لاحظ أن await
كأنها بديل ابسط للـ then
الـ async/await
جاء هنا لتبسيط شكل استعمالنا للـ Promise
وتجنب كتابة شكل الـ Promises chaining
الخاصة بتعدد الـ then
استخدام الـ try-catch
حسنًا، الآن في الـ promise
كنا عندما يحدث خطأ كنا نستخدم دالة الـ catch
للتعامل مع أي خطأ ونستطيع تفاديه
اما في الـ async/await
نستخدم try-catch
نضع الأوامر داخل الـ try
ونتعامل مع أي خطأ داخل الـ catch
بهذا الشكل
async function f1() {
try {
let res = await fetch('./file.json');
let data = await res.json();
// console.log(data);
console.log('f1 done at :', Date.now() - start);
} catch (err) {
console.log(err); // catch any errors here
}
}
مقارنة بين كل من Callback, Promise, Async/Await
سنقوم بعمل مقارنة مرئية للفرق بين اسلوب كتابة كل من الثلاثة
ابتداءًا من مشكلة الـ Callback hell
إلى الـ Promises chaining
نهايتًا إلى async/await
تذكرون دالة addTwo
من مقالة الـ promise
؟
function addTwo(x, y) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(x + y);
}, 3000);
});
}
دالة تقوم بجمع رقمين بعد ثلاث ثواني
الآن لنبدأ المقارنة
لدينا أولًا صديقنا اللدود Callback Hell
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)
});
});
});
});
});
});
});
});
مازال يحتفظ بهيبته كما ترى، هرم مقلوب من 8
أدوار
ثم قمنا بجعل كل promise
يقوم بإرجاع promise
وانتهى بنا المطاف إلى 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
طوابق
الآن كيف سيبدو شكل الكود إن استعملنا async/await
دعونا نرى
async function f() {
let result = await addTwo(5, 7);
result = await addTwo(result, 10);
result = await addTwo(result, 12);
result = await addTwo(result, 11);
result = await addTwo(result, 20);
result = await addTwo(result, 32);
result = await addTwo(result, 18);
result = await addTwo(result, 99);
console.log(result); // OUTPUT: 214 (after 24 second)
}
f(); // call f
أنظروا إلى الجمال والبساطة
أظن أن الكود أبلغ من الشرح ولا أحتاج لأقول شيء هنا
فقط نحن وضعنا الأكواد داخل دالة تدعى f
لكي نستطيع استخدام الـ async/await
لا أكثر
ملحوظة
: قد تسمع في المستقبل عن الـtop level await
وهو أن تقوم اللغة بدعم استخدامawait
بشكل مستقل دون أن تكون داخل دالة معمول لهاasync