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

طرق كتابة شيفرات غير متزامنة التنفيذ في Node.js


Hassan Hedr

البرمجة المتزامنة synchronous programming في جافاسكربت تعني تنفيذ التعليمات في الأسطر البرمجية سطرًا تلو الآخر بحسب ترتيب كتابتها تمامًا، ولكن لا حاجة للالتزام بترتيب التنفيذ هذا دومًا، فمثلًا عند إرسال طلب عبر الشبكة ستضطر الإجرائية التي يُنفذ فيها البرنامج إلى انتظار رد ذلك الطلب ووصول جوابه قبل أن نتمكن من إكمال تنفيذ باقي البرنامج، حيث وقت انتظار إتمام الطلب هذا هو وقت مهدور، هنا يأتي دور البرمجة اللامتزامنة asynchronous programming لتحل هذه المشكلة، حيث تُنفذ فيها الأسطر البرمجية للبرنامج بترتيب مختلف عن ترتيب كتابتها الأصلي، فيصبح بإمكاننا مثلًا في مثالنا السابق تنفيذ تعليمات برمجية أخرى في أثناء انتظار إتمام عملية إرسال الطلب ووصول جوابه المنتظر مع البيانات المطلوبة.

تٌنفَذ شيفرة جافاسكربت ضمن خيط وحيد thread ضمن الإجرائية، حيث تعالج شيفراتها بشكل متزامن ضمن ذلك الخيط عبر تنفيذ تعليمة واحدة فقط في كل لحظة، ويتوضح أثر البرمجة المتزامنة في هذه الحالة أكثر، فعند تنفيذ المهام التي تحتاج لوقت كبير ضمن ذلك الخيط سيُعيق ذلك تنفيذ كل الشيفرات اللاحقة لحين انتهاء تلك المهمة، لذا وبالاستفادة من مزايا برمجة جافاسكربت اللامتزامنة يمكننا إزاحة المهام التي تأخذ وقتًا طويلًا في التنفيذ إلى خيط آخر في الخلفية وبالتالي حل المشكلة، وبعد انتهاء تلك المهمة الطويلة تُنفذ الشيفرات المتعلقة بمعالجة بياناتها ضمن الخيط الأساسي لشيفرة جافاسكربت مجددًا.

سنتعلم في هذا المقال طرق إدارة المهام اللامتزامنة باستخدام حلقة الأحداث Event Loop الخاصة بجافاسكربت والتي تُنهي بواسطتها مهامًا جديدة أثناء انتظار انتهاء المهام الأخرى، ولذلك سنطور برنامجًا يستفيد من البرمجة اللامتزامنة لطلب قائمة من الأفلام من الواجهة البرمجية لاستديو Ghibli وحفظ بياناتها ضمن ملف CSV، حيث سننفذ ذلك بثلاثة طرق وهي دوال رد النداء callback functions والوعود promises وأخيرًا باستخدام اللاتزامن والانتظار async/await ومع أنه من غير الشائع حاليًا استخدام دوال رد النداء في البرمجة اللامتزامنة في جافاسكربت، إلا أنه من المهم تعلم تلك الطريقة لفهم تاريخ الانتقال لاستخدام الوعود ووجودها أساسًا، ثم تأتي آلية اللاتزامن والانتظار لتسمح باستخدام الوعود بطريقة أبسط، وهي الطريقة المعتمدة حاليًا عند كتابة الشيفرات اللامتزامنة في جافاسكربت.

المستلزمات

  • تثبيت بيئة Node.js على الجهاز، حيث استخدمنا في هذا المقال الإصدار رقم 10.17.0، ويمكنك الاطلاع على مقال تثبيت Node.js على نظام أبونتو 18.04 للتعرف على طريقة تثبيته.
  • معرفة طريقة تثبيت الحزم ضمن المشروع باستخدام npm.
  • معرفة طرق تعريف وتنفيذ الدوال البسيطة في جافاسكربت قبل تعلم تنفيذها بالطريقة اللامتزامنة.

حلقة الأحداث Event Loop

لنتعرف بدايةً على الطريقة التي ينفذ بها جافاسكربت الدوال داخليًا، ما سيسمح لنا لاحقًا بفهم أكثر عند كتابة الشيفرات اللامتزامنة وتزيد قدرتنا على استكشاف الأخطاء وتصحيحها حين حدوثها، حيث يضيف مفسر جافاسكربت كل دالة تُنفَّذ إلى مكدس الاستدعاءات call stack، وهو هيكلية بيانات شبيهة بالقائمة بحيث يمكن إضافة أو حذف العناصر منه من الأعلى فقط أي تعتمد مبدأ الداخل آخرًا يخرج أولًا LIFO -اختصارًا إلى Last in, first out- فعند إضافة عنصرين إلى المكدس مثلًا يمكن حذف آخر عنصر تمت إضافته أولًا، فمثلًا عند استدعاء الدالة ‎functionA()‎ سيُضاف ذلك إلى مكدس الاستدعاء، وإذا استدعت الدالة functionA()‎ داخلها دالة أخرى مثلًا functionB()‎ فسيضاف الاستدعاء الأخير لأعلى مكدس الاستدعاء، وبعد الانتهاء من تنفيذه سيُزال من أعلى مكدس الاستدعاء، أي ينفذ جافاسكربت أولًا الدالة functionB()‎ ثم يزيلها من المكدس عند انتهائها، ثم يُنهي تنفيذ الدالة الأب functionA()‎ ثم يزيلها أيضًا من مكدس الاستدعاء، لهذا يتم دومًا تنفيذ الدوال الأبناء أو الداخلية قبل الدوال الآباء أو الخارجية.

عندما يُنفذ جافاسكربت عملية لا متزامنة ككتابة البيانات إلى ملف مثلًا، فسيضيفها إلى جدول خاص ضمن الذاكرة يُخزَّن فيه العملية وشرط اكتمالها والدالة التي ستُستدعى عند اكتمالها، وبعد اكتمال العملية ستضاف تلك الدالة إلى رتل الرسائل message queue، وهو هيكلية بيانات تشبه القائمة أيضًا تُضاف إليها العناصر من الأسفل وتزال من الأعلى فقط أي تعتمد مبدأ الداخل أولًا يخرج أولًا FIFO -اختصارًا إلى First in, First out- وحين انتهاء عمليتين لا متزامنتين والتجهيز لاستدعاء الدوال الخاصة بهما سيتم استدعاء الدالة الخاصة بالعملية التي انتهت أولًا، حيث تنتظر الدوال ضمن رتل الرسائل إضافتها إلى مكدس الاستدعاء، وتبقى حلقة الأحداث في فحص دائم لمكدس الاستدعاء بانتظار فراغه، عندها يُنقل أول عنصر من رتل الرسائل إلى مكدس الاستدعاء، ويعطي جافاسكربت الأولوية للدوال ضمن رتل الرسائل بدلًا من استدعاءات الدوال الجديدة التي يفسرها ضمن الشيفرة، وبذلك تسمح تركيبة عمل مكدس الاستدعاء ورتل الرسائل وحلقة الأحداث بتنفيذ شيفرات جافاسكربت أثناء معالجة المهام اللامتزامنة.

والآن بعد أن ألقينا نظرة عامة على حلقة الأحداث وتعرفنا فيها على طريقة تنفيذ الشيفرات اللامتزامنة في جافاسكربت يمكننا البدء بكتابة شيفرات لا متزامنة باستخدام إحدى الطرق لذلك، إما بدوال رد النداء أو الوعود أو باستخدام اللاتزامن والانتظار async/await.

البرمجة اللامتزامنة باستخدام دوال رد النداء

تُمرَّر دالة رد النداء callback function كمعامل للدوال الأخرى وتُنفَذ عند انتهاء تنفيذ الدالة المُمررة لها، وتحوي دالة رد النداء عادة شيفرات لمعالجة نتيجة تلك العملية أو شيفرات لتنفيذها بعد انتهاء تنفيذ العملية اللامتزامنة، حيث استخدمت هذه الطريقة لفترة طويلة وكانت أشيع طريقة مستخدمة لكتابة الشيفرات اللامتزامنة، ولكنها لم تعد مستخدمة حاليًا لأنها تُصعّب قراءة ووضوح الشيفرة، لكن سنستخدمها في هذه الفقرة لكتابة شيفرة جافاسكربت لا متزامنة لنتعرف بذلك على كل الطرق الممكنة ونلاحظ الفروقات بينها وميزة كل منها، حيث نستخدم دوال رد النداء بأكثر من طريقة ضمن الدوال الأخرى، وعادة ما نُمرِّرها كآخر معامل للدالة اللامتزامنة كالتالي:

function asynchronousFunction([ Function Arguments ], [ Callback Function ]) {
    [ Action ]
}

لكننا لسنا ملزومين باتباع هذه البنية عند كتابة الدوال اللامتزامنة، ولكن شاع تمرير دالة رد النداء كآخر معامل للدالة اللامتزامنة ليسهل التعرف عليه بين المبرمجين، وتُمرَّر عادة دالة رد النداء كدالة مجهول الاسم -التي تُعرّف بلا اسم- إذ يُحسِّن تمريرها كآخر معامل قراءة الشيفرة، ولنفهم ذلك أكثر سننشئ وحدة برمجية في نود وظيفتها كتابة قائمة من أفلام استوديو Ghibli إلى ملف، فنبدأ بإنشاء مجلد سيحوي ملف جافاسكربت للبرنامج وملف الخرج النهائي كالتالي:

mkdir ghibliMovies

ندخل إلى المجلد:

cd ghibliMovies

سنرسل بدايةً طلب HTTP للواجهة البرمجية لاستديو Ghibli ونطبع داخل دالة رد النداء نتيجة ذلك الطلب، ولنتمكن من ذلك نحتاج إلى مكتبة تساعدنا في إرسال طلبات HTTP والوصول إلى البيانات ضمن الرد باستخدام دالة رد نداء، لذا نُهيئ ملف الحزمة للوحدة بتنفيذ الأمر التالي:

npm init -y

ونثبت مكتبة request بتنفيذ الأمر:

npm i request --save

ننشئ ملفًا جديدًا بالاسم callbackMovies.js ونفتحه باستخدام أي محرر نصوص:

nano callbackMovies.js

ونكتب داخله الشيفرة التالية والتي سترسل طلب HTTP باستخدام مكتبة request السابقة:

const request = require('request');

request('https://ghibliapi.herokuapp.com/films');

نُحمّل في أول سطر مكتبة request التي ثبتناها، حيث ستعيد المكتبة دالة يمكن استدعاؤها لإنشاء طلبات HTTP نخزنها ضمن الثابت request، ثم نرسل طلب HTTP باستدعاء الدالة request()‎ وتمرير عنوان الواجهة البرمجية API له.

لنطبع الآن البيانات من نتيجة الطلب إلى الطرفية بإضافة الأسطر كالتالي:

const request = require('request');

request('https://ghibliapi.herokuapp.com/films', (error, response, body) => {
    if (error) {
        console.error(`Could not send request to API: ${error.message}`);
        return;
    }

    if (response.statusCode != 200) {
        console.error(`Expected status code 200 but received ${response.statusCode}.`);
        return;
    }

    console.log('Processing our list of movies');
    movies = JSON.parse(body);
    movies.forEach(movie => {
        console.log(`${movie['title']}, ${movie['release_date']}`);
    });
});

مررنا للدالة request()‎ معاملان هما عنوان URL للواجهة البرمجية API لإرسال الطلب إليها، ودالة رد نداء سهمية لمعالجة أي أخطاء قد تحدث أو معالجة نتيجة إرسال الطلب عند نجاحه بعد انتهاء تنفيذه.

لاحظ أن دالة رد النداء أخذت ثلاثة معاملات وهي كائن الخطأ error و كائن الرد response وبيانات جسم الطلب body، فبعد اكتمال الطلب سيُعيَّن قيم لتلك المعاملات بناءً على النتيجة، ففي حال فشل الطلب سيتم تعيين قيمة كائن للمعامل error، وتعيين القيمة null لكل من response و body، وعند نجاح الطلب سيتم تعيين قيمة الرد للمعامل response، وفي حال احتوى الرد على بيانات في جسم الطلب ستُعيَّن كقيمة للمعامل body.

ونستفيد من تلك المعاملات داخل دالة رد النداء التي مَرَّرناها لها للتحقق من وجود الخطأ أولًا، ويفضل التحقق من ذلك دومًا ضمن دوال رد النداء بحيث لا نكمل تنفيذ باقي التعليمات عند حدوث خطأ، وفي حال وجود خطأ سنطبع رسالة الخطأ إلى الطرفية وننهي تنفيذ الدالة، بعدها نتحقق من رمز الحالة للرد المرسل ففي حال عدم توافر الخادم للرد على الطلب أو تغيير الواجهة البرمجية أو إرسال طلبية خاطئة سنلاحظ ذلك من رمز الرد ويمكن التحقق من نجاح العملية وسلامة الرد بالتحقق من أن رمز الحالة يساوي 200 ويمكننا بذلك متابعة معالجة الطلب، وفي حالتنا عالجنا الطلب بتحليل الرد وتحويله إلى مصفوفة ثم طبع كل عنصر من عناصرها -التي تمثل الأفلام- على شكل اسم الفلم وتاريخ إصداره، والآن نحفظ الملف وتخرج منه وننفذه كالتالي:

node callbackMovies.js

ليظهر الخرج كالتالي:

Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014

حصلنا على قائمة بأفلام من إنتاج استوديو Ghibli مع تواريخ إصدارها بنجاح، والآن نريد من البرنامج كتابة القائمة إلى ملف، لذا نعدل الملف callbackMovies.js ضمن محرر النصوص ونضيف الأسطر التالية لإنشاء ملف بصيغة CSV يحوي بيانات الأفلام المجلوبة:

const request = require('request');
const fs = require('fs');

request('https://ghibliapi.herokuapp.com/films', (error, response, body) => {
    if (error) {
        console.error(`Could not send request to API: ${error.message}`);
        return;
    }

    if (response.statusCode != 200) {
        console.error(`Expected status code 200 but received ${response.statusCode}.`);
        return;
    }

    console.log('Processing our list of movies');
    movies = JSON.parse(body);
    let movieList = '';
    movies.forEach(movie => {
        movieList += `${movie['title']}, ${movie['release_date']}\n`;
    });

    fs.writeFile('callbackMovies.csv', movieList, (error) => {
        if (error) {
            console.error(`Could not save the Ghibli movies to a file: ${error}`);
            return;
        }

        console.log('Saved our list of movies to callbackMovies.csv');;
    });
});

نلاحظ بدايةً استيراد الوحدة البرمجية fs والتي توفرها بيئة نود للتعامل مع الملفات حيث نريد التابع writeFile()‎ منها لكتابة البيانات إلى ملف بطريقة لا متزامنة، وبدلًا من طباعة البيانات إلى الطرفية، يمكننا إضافتها إلى السلسلة النصية للمتغير movieList ثم نستدعي التابع writeFile()‎ لحفظ قيمة movieList النهائية إلى ملف جديد بالاسم callbackMovies.csv، ثم نمرر أخيرًا دالة رد نداء للتابع writeFile()‎ حيث سيُمرَّر لها معاملًا وحيدًا وهو كائن الخطأ error نعرف بالتحقق منه إذا ما فشلت عملية الكتابة إلى الملف، وذلك مثلًا عندما لا يملك المستخدم الحالي الذي يُنفِّذ إجرائية نود صلاحيات كافية لإنشاء ملف جديد ضمن المسار الحالي.

والآن نحفظ الملف وننفذه مرة أخرى:

node callbackMovies.js

سنلاحظ ظهور ملف جديد ضمن مجلد المشروع ghibliMovies بالاسم callbackMovies.csv يحوي قائمة أفلام تشبه القائمة التالية:

Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014

نلاحظ أننا كتبنا ذلك المحتوى إلى ملف CSV ضمن دالة رد النداء لطلب HTTP المرسل، حيث أن الشيفرات ضمن تلك الدالة ستُنفَذ بعد انتهاء عملية إرسال الطلب فقط، وفي حال أردنا الاتصال بقاعدة بيانات بعد كتابة محتوى ملف CSV السابق يجب إنشاء دالة لا متزامنة أخرى تُستدعَى ضمن دالة رد نداء التابع writeFile()‎، وكلما أردنا تنفيذ عمليات لا متزامنة متلاحقة يجب تغليف المزيد من دوال رد النداء داخل بعضها البعض، فإذا أردنا مثلًا تنفيذ خمس عمليات لا متزامنة متتالية بحيث تُنفًّذ كل منها بعد انتهاء العملية التي تسبقها وسنحصل في النهاية على شيفرة بنيتها تشبه التالي:

doSomething1(() => {
    doSomething2(() => {
        doSomething3(() => {
            doSomething4(() => {
                doSomething5(() => {
                    // العملية النهائية
                });
            });
        }); 
    });
});

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

استخدام الوعود لاختصار الشيفرات اللامتزامنة

الوعد Promise هو كائن توفره جافاسكربت وظيفته إرجاع قيمة ما مستقبلًا ومن هنا جاءت تسمية الوعد من أنه يعدك بإعادة قيمة ما لاحقًا، ويمكن للدوال اللامتزامنة أن تُعيد كائن وعد من هذا النوع بدلًا من إرجاع القيمة النهائية لتنفيذها، وعند تحقق هذا الوعد مستقبلًا fulfilled سنحصل على القيمة النهائية للعملية وإلا سنحصل على خطأ ويكون الوعد قد رُفض rejected، أما خلال التنفيذ يكون الوعد في حالة الانتظار ويتم معالجته.

تستخدم الوعود بالصيغة التالية:

promiseFunction()
    .then([ رد نداء يُنفَّذ عند تحقق الوعد ])
    .catch([ رد نداء يُنفَّذ عند رفض الوعد ])

نلاحظ أن الوعود تستخدم دوال رد النداء هي أيضًا، حيث نُمرِّر للتابع then()‎ دالة رد نداء تُستدعى عند نجاح التنفيذ، ونُمرِّر للتابع catch()‎ دالة رد نداء أخرى تُستدعى لمعالجة الأخطاء عند حدوثها أثناء عملية تنفيذ ذلك الوعد.

ولنتعرف على الوعود أكثر سنطور برنامجنا السابق لاستخدام طريقة الوعود بدلًا من دوال رد النداء، ونبدأ بتثبيت مكتبة Axios التي تعتمد على الوعود في عملياتها لإرسال طلبات HTTP:

npm i axios --save

نُنشئ ملفًا جديدًا بالاسم promiseMovies.js سيحوي النسخة الجديدة من البرنامج:

nano promiseMovies.js

سنرسل طلب HTTP باستخدام مكتبة axios هذه المرة، وباستخدام نسخة خاصة من وحدة fs تعتمد في عملها على الوعود سنحفظ النتيجة ضمن ملف CSV كما فعلنا سابقًا، ونبدأ بكتابة الشيفرة التالية ضمن الملف لتحميل مكتبة Axios وإرسال طلب HTTP للواجهة البرمجية للحصول على قائمة الأفلام:

const axios = require('axios');

axios.get('https://ghibliapi.herokuapp.com/films');

حملنا في أول سطر مكتبة axios وحفظنا الناتج ضمن الثابت axios وبعدها استدعينا التابع axios.get()‎ لإرسال طلب HTTP إلى الواجهة البرمجية، حيث سيعيد التابع axios.get()‎ وعدًا يمكننا ربطه مع دالة لطباعة الأفلام إلى الطرفية عند نجاح الطلب كالتالي:

const axios = require('axios');


axios.get('https://ghibliapi.herokuapp.com/films')
    .then((response) => {
        console.log('Successfully retrieved our list of movies');
        response.data.forEach(movie => {
            console.log(`${movie['title']}, ${movie['release_date']}`);
        });
    })

بعد إرسال طلب HTTP من نوع GET باستخدام التابع axios.get()‎ استخدمنا التابع then()‎ والذي سيُنفذ عند نجاح الطلب فقط، وطبعنا داخله الأفلام إلى الطرفية كما فعلنا في الفقرة السابقة، والآن نطور البرنامج لكتابة تلك البيانات إلى ملف جديد باستخدام واجهة للتعامل مع نظام الملفات قائمة على الوعود كالتالي:

const axios = require('axios');
const fs = require('fs').promises;


axios.get('https://ghibliapi.herokuapp.com/films')
    .then((response) => {
        console.log('Successfully retrieved our list of movies');
        let movieList = '';
        response.data.forEach(movie => {
            movieList += `${movie['title']}, ${movie['release_date']}\n`;
        });

        return fs.writeFile('promiseMovies.csv', movieList);
    })
    .then(() => {
        console.log('Saved our list of movies to promiseMovies.csv');
    })

استوردنا الوحدة البرمجية fs مجددًا لكن نلاحظ استخدام الخاصية ‎.promises منها، وهي النسخة الخاصة من وحدة fs التي تستخدم الوعود كنتيجة لتنفيذ دوالها بدلًا من طريقة دوال رد النداء، وسبب إتاحتها كنسخة منفصلة هو دعم المشاريع التي لازالت تستخدم الطريقة القديمة.

ونلاحظ كيف أصبح أول استدعاء للتابع then()‎ يعالج رد الطلب HTTP الوارد ثم يستدعي التابع fs.writeFile()‎ بدلًا من طباعة البيانات إلى الطرفية، وبما أننا نستخدم نسخة الوعود من fs فسيعيد التابع writeFile()‎ عند استدعائه وعدًا آخر، يجري معالجته باستدعاء then()‎ مرة أخرى والتي بدورها تأخذ دالة رد النداء تُنفَّذ عند نجاح تنفيذ ذلك الوعد -المُعاد من التابع writeFile()‎.

نلاحظ أيضًا مما سبق أنه يمكن إعادة وعد من داخل وعد آخر، ما سيسمح بتنفيذ تلك الوعود الواحد تلو الآخر، ويوفر لنا ذلك طريقة لتنفيذ عدد من العمليات اللامتزامنة خلف بعضها البعض، وندعو هذه العملية باسم سلسلة الوعود promise chaining وهي بديل عن استخدام دوال رد النداء المتداخلة التي تعرفنا عليها في الفقرة السابقة، بحيث يُستدعى التابع then()‎ الموالي عند تحقق الوعد المعاد من سابقه وهكذا وعند رفض أحد الوعود يُستدعى التابع catch()‎ مباشرةً آنذاك وتتوقع السلسلة عن العمل.

ملاحظة: لم نتحقق في هذا المثال من رمز الرد لطلب HTTP الوارد كما فعلنا سابقًا، حيث لن يُلبي axios تلقائيًا الوعد الذي يعيده في حال كان رمز الرد الوارد يمثل أي خطأ، ولذلك لم نعد مضطرين للتحقق منه بأنفسنا.

والآن نضيف التابع catch()‎ في نهاية البرنامج لإكماله كالتالي:

const axios = require('axios');
const fs = require('fs').promises;


axios.get('https://ghibliapi.herokuapp.com/films')
    .then((response) => {
        console.log('Successfully retrieved our list of movies');
        let movieList = '';
        response.data.forEach(movie => {
            movieList += `${movie['title']}, ${movie['release_date']}\n`;
        });

        return fs.writeFile('promiseMovies.csv', movieList);
    })
    .then(() => {
        console.log('Saved our list of movies to promiseMovies.csv');
    })
    .catch((error) => {
        console.error(`Could not save the Ghibli movies to a file: ${error}`);
    });

في حال فشل أي وعد من سلسلة الوعود تلك سيُنفذ التابع catch()‎ تلقائيًا كما أشرنا متجاوزًا أي دوال تسبقه، لذا يمكننا إضافة استدعاء التابع catch()‎ مرة واحدة فقط في النهاية لمعالجة أي خطأ قد يحدث من أي عملية سابقة حتى لو كنا ننفذ عدة عمليات غير متزامنة متتالية.

والآن لنتحقق من صحة عمل البرنامج بتنفيذه كالتالي:

node promiseMovies.js

نلاحظ ظهور نفس البيانات السابقة ضمن الملف promiseMovies.csv:

Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014

نلاحظ كيف اختصر استخدام الوعود كتابة الكثير من الشيفرات، وكيف أن عملية سَلسَلة الوعود أسهل وأبسط ومقروءة أكثر من طريقة دوال رد النداء المتداخلة، ولكن حتى مع تلك المزايا الجديدة هنالك صعوبات تحصل في حال أردنا تنفيذ العديد من العمليات اللامتزامنة أي ستزداد صعوبة الشيفرة المكتوبة بازدياد طول سلسلة الوعود.

وتحتاج كلا الطريقتين السابقتين سواء دوال رد النداء أو الوعود لإنشاء دوال رد نداء تُعالج ناتج العملية اللامتزامنة، والطريقة الأفضل من ذلك هي انتظار نتيجة العملية اللامتزامنة وتخزينها ضمن متغير خارج أي دالة، وبذلك يمكننا استخدام النتائج ضمن المتغيرات مباشرةً ودون الحاجة لإنشاء الكثير من الدوال في كل مرة، وهذا تحديدًا ما يميز عملية اللاتزامن والانتظار باستخدام async و await في جافاسكربت وهي ما سنتعرف عليه في الفقرة التالية.

ملاحظة: إن أردت تعلم المزيد حول الوعود، فارجع إلى توثيق واجهة الوعود Promise في موسوعة حسوب.

التعامل مع الوعود باستخدام طريقة اللاتزامن والانتظار async/await

تتيح الكلمة المفتاحية async اللاتزامن والكلمة المفتاحية await الانتظار صيغة بديلة أبسط للتعامل مع الوعود، إذ ستُعاد النتيجة مباشرةً كقيمة بدلًا من تمريرها على شكل وعد إلى التابع then()‎ لمعالجتها وكأننا نستدعي تابع متزامن عادي في جافاسكربت.

ولنخبر جافاسكربت أن دالة ما هي دالة لا متزامنة تُعيد وعدًا، نعرفها بوضع الكلمة المفتاحية async قبلها، وبعدها يمكننا استخدام الكلمة المفتاحية await داخلها لإخبار جافاسكربت بإرجاع ناتج الوعد المُعاد عند نجاحه بدلًا من إرجاع الوعد نفسه كقيمة، أي تكون صيغة استخدام async/await كالتالي:

async function() {
    await [عملية غير متزامنة]
}

لنطبق استخدامها على برنامجنا ونلاحظ الفرق، لننشئ ملفًا للبرنامج الجديد بالاسم asyncAwaitMovies.js:

nano asyncAwaitMovies.js

نستورد داخل ذلك الملف نفس الوحدات التي استخدمناها سابقًا لأن طريقة async/await تعتمد على الوعود في عملها:

const axios = require('axios');
const fs = require('fs').promises;

والآن نعرّف دالة باستخدام الكلمة المفتاحية async للدلالة على أنها دالة لا متزامنة كالتالي:

const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {}

عرفنا الدالة saveMovies()‎ باستخدام الكلمة المفتاحية async، بهذا نستطيع استخدام الكلمة المفتاحية await داخلها، أي ضمن الدوال اللامتزامنة التي نعرفها بنفس تلك الطريقة، والآن نستخدم الكلمة المفتاحية await لإرسال طلب HTTP إلى الواجهة البرمجية لجلب قائمة الأفلام:

const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {
    let response = await axios.get('https://ghibliapi.herokuapp.com/films');
    let movieList = '';
    response.data.forEach(movie => {
        movieList += `${movie['title']}, ${movie['release_date']}\n`;
    });
}

نرسل طلب HTTP باستخدام axios.get()‎ من داخل الدالة saveMovies()‎ كما فعلنا سابقًا، لكن لاحظ أنه بدلًا من استدعاء التابع then()‎ أضفنا الكلمة المفتاحية await قبل الاستدعاء، سينفذ حينها جافاسكربت الشيفرة في الأسطر اللاحقة فقط عند نجاح تنفيذ التابع axios.get()‎، وستُعيَّن القيمة التي يعيدها إلى المتغير response، والآن نضيف الشيفرة المسؤولة عن كتابة البيانات الواردة إلى ملف CSV:

const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {
    let response = await axios.get('https://ghibliapi.herokuapp.com/films');
    let movieList = '';
    response.data.forEach(movie => {
        movieList += `${movie['title']}, ${movie['release_date']}\n`;
    });
    await fs.writeFile('asyncAwaitMovies.csv', movieList);
}

نلاحظ استخدامنا للكلمة المفتاحية await عند استدعاء التابع fs.writeFile()‎ أيضًا لكتابة محتويات الملف، والآن ننهي كتابة الدالة بالتقاط ومعالجة أي أخطاء قد ترميها تلك العمليات باستخدام try/catch كما نفعل عادةً في جافاسكربت لالتقاط الأخطاء المرمية:

const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {
    try {
        let response = await axios.get('https://ghibliapi.herokuapp.com/films');
        let movieList = '';
        response.data.forEach(movie => {
            movieList += `${movie['title']}, ${movie['release_date']}\n`;
        });
        await fs.writeFile('asyncAwaitMovies.csv', movieList);
    } catch (error) {
        console.error(`Could not save the Ghibli movies to a file: ${error}`);
    }
}

وبذلك نضمن معالجة الأخطاء عند حدوثها في العمليات اللامتزامنة داخل جسم try، بما فيها أي أخطاء قد تحدث عند إرسال طلب HTTP أو عند فشل الكتابة إلى الملف.

والآن نستدعي الدالة saveMovies()‎ اللامتزامنة لضمان تنفيذها عند تنفيذ البرنامج باستخدام نود:

const axios = require('axios');
const fs = require('fs').promises;

async function saveMovies() {
    try {
        let response = await axios.get('https://ghibliapi.herokuapp.com/films');
        let movieList = '';
        response.data.forEach(movie => {
            movieList += `${movie['title']}, ${movie['release_date']}\n`;
        });
        await fs.writeFile('asyncAwaitMovies.csv', movieList);
    } catch (error) {
        console.error(`Could not save the Ghibli movies to a file: ${error}`);
    }
}

saveMovies();

لا يوجد فروقات كبيرة بين هذه الطريقة وبين الصيغة العادية لكتابة واستدعاء شيفرات جافاسكربت المتزامنة، حيث لم نحتاج لتعريف العديد من الدوال -تحديدًا دوال ردود النداء- وتمريرها كما فعلنا سابقًا، وتتوضح بذلك ميزة استخدام async/await تسهيل قراءة واستخدام الشيفرات اللامتزامنة أفضل من الطرق الأخرى.

والآن ننفذ هذا البرنامج ونختبر عمله:

node asyncAwaitMovies.js

نلاحظ ظهور ملف جديد بالاسم asyncAwaitMovies.csv ضمن مجلد المشروع ghibliMovies يحوي داخله على التالي:

Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014

وبذلك نكون تعرفنا على طريقة عملها استخدام ميزة async/await في جافاسكربت.

ختامًا

تعرفنا في هذا المقال على الطريقة التي تعالج بها جافاسكربت الدوال وتدير العمليات اللامتزامنة باستخدام حلقة الأحداث، وكتبنا برنامجًا لإنشاء ملف بصيغة CSV بالاعتماد على بيانات واردة من واجهة برمجية API بإرسال طلب HTTP نطلب فيه بيانات عدد من الأفلام مستخدمين بذلك كل طرق البرمجة اللامتزامنة المتوفرة في جافاسكربت، حيث بدأنا ذلك باستخدام طريقة دوال رد النداء القديمة وبعدها تعرفنا على الوعود وطريقة استخدامها، ثم طورنا ذلك باستخدام طريقة اللاتزامن والانتظار async/await لتصبح الشيفرة أبسط وأوضح.

ويمكنك الآن بعد ما تعلمته ضمن هذا المقال استخدام التقنيات التي تعلمتها لكتابة البرامج التي تستخدم العمليات اللامتزامنة، ويمكنك الاستفادة من قائمة الواجهات البرمجية العامة المتاحة لتطوير ما قد يفيدك، وذلك بإرسال طلبات HTTP لا متزامنة إليها كما فعلنا في هذا المقال.

ترجمة -وبتصرف- للمقال How To Write Asynchronous Code in Node.js.

اقرأ أيضًا


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

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

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



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

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

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

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


×
×
  • أضف...