Skip to content

11. البرمجة القائمة على الأحداث والوظائف غير المتزامنة

Image

الوظيفة غير المتزامنة هي وظيفة يتم تشغيلها دون انتظار النتيجة. عند انتهاء التنفيذ، تصدر الوظيفة غير المتزامنة حدثًا وتنقل نتيجتها عبره.

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

سيتم تنفيذ البرامج النصية التالية بواسطة [node.js] وليس بواسطة متصفح. كما أن [node.js] لديه أيضًا وضع تنفيذ حسب الحدث:

  • يستخدم [node.js]، مثل المتصفح، حلقة أحداث لتنفيذ البرنامج النصي؛
  • يُعد تنفيذ الكود الرئيسي للبرنامج النصي هو الحدث الأول الذي يتم تنفيذه؛
  • إذا أطلق هذا الكود الرئيسي مهام غير متزامنة، فإن التنفيذ يستمر حتى تنتهي هذه المهام غير المتزامنة. وتصدر هذه المهام حدثًا عند انتهائها. يتم وضع هذه الأحداث في قائمة انتظار في حلقة الأحداث؛
  • يجب أن يشترك البرنامج النصي الرئيسي في هذه الأحداث إذا أراد استرداد نتائج الإجراءات غير المتزامنة؛
  • لا ينتهي البرنامج النصي إلا بعد معالجة جميع الأحداث التي أصدرها؛

11.1. البرنامج النصي [async-01]

يوضح البرنامج النصي التالي سلوك برنامج نصي يتضمن إجراءً غير متزامن.


'use strict';

// الاستيرادات
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// البداية
const débutScript = moment(Date.now());
console.log("[début du script],", heure());

// setTimeout يضبط مؤقتًا لمدة 1000 مللي ثانية (المعلمة الثانية) ويعيد على الفور رقم هذا المؤقت
// عندما ينتهي المؤقت من 1000 مللي ثانية، فإنه يصدر حدثًا يتم وضعه في قائمة انتظار وقت التشغيل
// عندما يتم معالجة الحدث بواسطة وقت التشغيل، يتم تنفيذ الدالة (المعلمة الأولى)
setTimeout(function () {
  // سيتم تنفيذ هذا الرمز عندما يصل المؤقت إلى القيمة 0
  console.log("[fin de l'action asynchrone setTimeout],", heure(débutScript));
}, 1000)

// سيتم عرضه قبل رسالة الدالة الداخلية للمؤقت
console.log("[fin du code principal du script],", heure(débutScript));

// أداة عرض الوقت والمدة
function heure(début) {
  // الوقت الحالي
  const now = moment(Date.now());
  // تنسيق الوقت
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  // هل يجب حساب المدة؟
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // تنسيق الوقت + المدة
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // النتيجة
  return result;
}
  • السطر 4: يتم استيراد المكتبة [moment] لتتمكن من تنسيق التواريخ (السطر 27)؛
  • السطر 5: يتم استيراد المكتبة [sprintf-js] لتتمكن من تنسيق المدد الزمنية (السطر 34)؛
  • السطر 8: يتم تسجيل وقت بدء البرنامج النصي؛
  • السطر 9: يتم عرضها باستخدام الطريقة [heure] في الأسطر 20-34؛
  • الأسطر 14-17: تحتوي الدالة [setTimeout] على معلمتين (f، المدة): f هي دالة يتم تنفيذها عند مرور [durée] ميلي ثانية؛
  • السطر 14: عند تنفيذ البرنامج النصي، يتم تنفيذ الدالة [setTimeout]:
    • السطر 17: يتم تفعيل مؤقت مدته 1000 مللي ثانية ويبدأ العد التنازلي حتى يصل لاحقًا إلى 0. تنتهي الدالة [setTimeout] بمجرد تهيئة المؤقت وبدء العد التنازلي. وهي لا تنتظر انتهاء هذا العد التنازلي. بل تُرجع رقمًا يُعرف المؤقت المستخدم، ثم ينتقل التنفيذ إلى التعليمات التالية، السطر 20. هنا، لا يتم استخدام نتيجة [setTimeout]؛
  • السطر 16: ستظهر هذه الرسالة في نهاية مهلة 1000 مللي ثانية للدالة [setTimeout]؛
  • السطران 15-16: سيتم تنفيذ الدالة f، المعلمة الأولى للدالة [setTimeout]، عند انتهاء مهلة 1000 مللي ثانية. ثم ستظهر الرسالة الموجودة في السطر 16؛
  • السطر 20: ستظهر هذه الرسالة قبل تلك الموجودة في السطر 16؛

دالة الوقت:

  • السطر 23: تقبل الدالة معلمة اختيارية [heure] وهي وقت بدء العملية التي يجب أن تعرض مدتها؛
  • الأسطر 25-27: يتم حساب وتنسيق الوقت الحالي؛
  • السطر 29: إذا كان المعامل [début] موجودًا، فيجب حساب المدة؛
  • السطر 30: مدة العملية. نحصل على عدد من الميلي ثانية؛
  • السطور 31-32: يتم تقسيم عدد الميلي ثانية هذا إلى ثوانٍ وميلي ثانية؛
  • السطر 34: تُضاف المدة إلى الساعة؛

التنفيذ


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-01.js"
[début du script], heure=09:26:40:238
[fin du code principal du script], heure=09:26:40:246, durée= 0 seconde(s) et 11 millisecondes
[fin de l'action asynchrone setTimeout], heure=09:26:41:249, durée= 1 seconde(s) et 14 millisecondes

[Done] exited with code=0 in 1.672 seconds
  • السطر 4، نرى أن الإجراء غير المتزامن [setTimeOut] قد انتهى بعد حوالي ثانية واحدة من انتهاء الكود الرئيسي للبرنامج النصي؛
  • السطر 6: الوقت المعروض في السطر 3 هو وقت انتهاء الكود الرئيسي. إذا كان هذا الكود قد أطلق مهام غير متزامنة، فإن البرنامج النصي لا ينتهي إلا بعد تنفيذ جميع المهام غير المتزامنة. المدة المعروضة في السطر 6 هي المدة الإجمالية لتنفيذ البرنامج النصي (الكود الرئيسي + المهام غير المتزامنة)؛

ستسمح لنا الدالة [setTimeout] بمحاكاة المهام غير المتزامنة في بيئة [node.js]. في الواقع، تتصرف الدالة [setTimeout] كمهمة غير متزامنة:

  • تُرجع نتيجة على الفور، وهي في هذه الحالة رقم مؤقت، من خلال آلية الدوال المعتادة (return
  • ويمكنها أن تعرض لاحقًا (وهذا ليس الحال في المثال أعلاه) نتائج أخرى عبر أحداث يتم معالجتها بعد ذلك بواسطة حلقة أحداث [node.js]؛
  • في معظم الحالات التالية، سيكون هناك حدثان:
    • حدث يمكن تسميته [success] سيصدر عن المهمة غير المتزامنة التي نجحت في إنجاز ما كان عليها القيام به. ترتبط بيانات، وهي نتيجة المهمة، بالحدث الصادر؛
    • حدث يمكن تسميته [failure] والذي سيتم إصداره من قبل المهمة غير المتزامنة التي فشلت في إنجاز ما كان عليها القيام به. ترتبط بيانات، وهي عادةً كائن يصف الخطأ، بالحدث الذي تم إصداره. من الأمثلة على الأخطاء المحتملة في مهمة الإنترنت غير المتزامنة: "الشبكة غير متاحة"، "الخادم غير موجود"، "تجاوز مهلة الانتظار"، ...
  • يمكن للكود الرئيسي الذي أطلق مهمة غير متزامنة الاشتراك في الأحداث التي من المحتمل أن تصدرها هذه المهمة. وعندما يتم إصدار أحد هذه الأحداث، يتم إخطار الكود الرئيسي بذلك ويمكنه تشغيل وظيفة معينة مخصصة لمعالجة الحدث. تتلقى هذه الوظيفة كمعلمة البيانات التي ربطتها المهمة غير المتزامنة بالحدث الصادر؛

11.2. نص برمجي [async-02]

في هذا البرنامج النصي، ستصدر الوظيفة غير المتزامنة [setTimeout] أحداثًا لإرسال البيانات إلى الرموز التي اشتركت في هذه الأحداث.

يتطلب الوصول إلى أحداث [node.js] مكتبات إضافية. نختار المكتبة [events] التي نقوم بتثبيتها مع [npm]:

Image

النص البرمجي [async-02] هو التالي:


'use strict';

// يمكن للوظائف غير المتزامنة إرجاع نتيجة عن طريق إصدار حدث
// يمكن للكود الرئيسي استرداد هذه النتائج من خلال الاشتراك في الأحداث التي تم إرسالها

// الاستيرادات
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';

// بداية
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// مرسل أحداث
const eventEmitter = new EventEmitter();

// setTimeout يضبط مؤقتًا لمدة 1000 مللي ثانية (المعلمة الثانية) ويعيد على الفور رقم هذا المؤقت
// عندما ينتهي المؤقت من 1000 مللي ثانية، فإنه يرسل حدثًا يتم وضعه في قائمة انتظار وقت التشغيل
// عندما تتم معالجة الحدث بواسطة وقت التشغيل، يتم تنفيذ الدالة (المعلمة الأولى)
setTimeout(function () {
  // سيتم تنفيذ هذا الرمز عندما يصل المؤقت إلى القيمة 0
  console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
  // يتم إرسال حدث للإشارة إلى توفر نتيجة
  eventEmitter.emit("timer1Success", { success: 4 });
  // يتم إرسال حدث آخر للإشارة إلى توفر نتيجة أخرى
  eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)

// يتم الاشتراك في الحدث [timer1Success]
eventEmitter.on('timer1Success', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});

// يتم الاشتراك في الحدث [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});

// سيتم عرضه قبل رسائل الأحداث الصادرة عن الوظيفة المرتبطة بـ [timer1]
console.log("[fin du code principal du script],", heure(débutScript));

// أداة عرض الوقت والمدة
function heure(début) {
  // الوقت الحالي
  const now = moment(Date.now());
  // تنسيق الوقت
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  // هل يجب حساب المدة؟
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // تنسيق الوقت + المدة
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // النتيجة
  return result;
}

تعليقات

  • السطر 9، يتم استيراد الفئة [EventEmitter] من المكتبة [events]. هذه ميزة جديدة: لم نكن حتى الآن نستورد سوى الكائنات الحرفية والوظائف؛
  • السطر 15: يتم إنشاء مُصدر أحداث [node.js] عن طريق إنشاء مثيل للفئة [EventEmitter] باستخدام الكلمة الرئيسية [new]؛
  • الأسطر 20-27: الدالة غير المتزامنة [setTimeout]. ستصدر هذه الدالة حدثين عند تنفيذها:
    • السطر 24، الحدث [timer1Success] مع القيمة المرتبطة به {success : 4}؛
    • السطر 26، الحدث [timer1Failure] مع قيمة مرتبطة به هي الكائن {failure : 6}؛
    • يمكن للدالة غير المتزامنة إصدار أي عدد تريده من الأحداث. ذكرنا سابقًا أنها غالبًا ما تصدر أحد الحدثين [success, failure]، وليس كليهما كما هو الحال هنا؛
  • السطر 20: يتم تنفيذ [setTimeout] على الفور: يتم ضبط مؤقت ويتم إرجاع رقمه إلى الكود المستدعي. سيتم إصدار الأحداث لاحقًا، هنا بعد ثانية واحدة؛
  • إرسال الأحداث لا فائدة منه إذا لم يكن هناك أي كود لاستغلالها عند حدوثها. لهذا السبب يجب أن يشترك الكود الرئيسي في الحدثين [timer1Success, timer1Failure] إذا أراد إدارتهما، ولا سيما استرداد البيانات المرتبطة بهذين الحدثين؛
  • الأسطر 30-32: يشترك الكود الرئيسي في الحدث [timer1Success]. عندما يعالج منفذ أحداث [node.js] هذا الحدث، فإنه سيستدعي الدالة التي تمثل المعلمة الثانية لطريقة [eventEmitter.on]، ممرراً إليها البيانات (المسماة هنا [result]) المرتبطة بالحدث [timer1Success]؛
  • السطر 31: ستعرض وظيفة معالجة الحدث jSON للبيانات المرتبطة بالحدث بالإضافة إلى الوقت الحالي؛
  • الأسطر 35-37: باستخدام كود مشابه، يشترك الكود الرئيسي في الحدث [timer1Failure]؛
  • لا يؤدي الاشتراك في حدث ما (المعلمة الأولى) إلى تنفيذ كود الدالة [callback] (المعلمة الثانية) على الفور. ولن يتم تنفيذ هذه الدالة إلا بعد وقوع الحدث؛
  • السطر 40: انتهى كود البرنامج النصي الرئيسي ولكن لم ينته البرنامج النصي نفسه لأن الكود الرئيسي أطلق مهمة غير متزامنة. لن ينتهي البرنامج النصي الشامل إلا بعد انتهاء هذه المهمة غير المتزامنة؛

وهذا ما تظهره النتائج التي تم الحصول عليها:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-02.js"
[début du script], heure=09:34:58:909
[fin du code principal du script], heure=09:34:58:916, durée= 0 seconde(s) et 10 millisecondes
[setTimeout, fin du timer d'1 s], heure=09:34:59:929, durée= 1 seconde(s) et 23 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=09:34:59:931, durée= 1 seconde(s) et 25 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=09:34:59:932, durée= 1 seconde(s) et 26 millisecondes, via l'événement [timer1Failure]

[Done] exited with code=0 in 1.627 seconds

  • السطر 3: نهاية الكود الرئيسي بعد 10 مللي ثانية من بدء البرنامج النصي؛
  • السطر 4: بداية الوظيفة المُغلفة في المؤقت الذي مدته 1000 مللي ثانية، بعد حوالي ثانية واحدة من بدء البرنامج النصي؛
  • السطر 5: معالجة الحدث [‘timer1Success’]، بعد 2 مللي ثانية؛
  • السطر 6: معالجة الحدث [‘timer1Failure’]، بعد 1 مللي ثانية من الحدث [‘timer1Success’]؛
  • السطر 8: نهاية البرنامج النصي الشامل بمدة إجمالية تبلغ 1.627 ثانية؛

11.3. النص البرمجي [async-03]

يُظهر البرنامج النصي التالي جانبًا آخر من حلقة الأحداث لـ [node.js]:

  • تقوم الحلقة بتنفيذ الأحداث واحدًا تلو الآخر، عادةً حسب ترتيب وصولها. بعض OS تمنح الأولوية للأحداث التي يتم معالجتها بعد ذلك حسب الأولوية وليس حسب ترتيب الوصول؛
  • لا تنفذ الحلقة سوى حدث واحد في كل مرة. ولا تتم معالجة الحدث التالي إلا بعد الانتهاء من معالجة الحدث السابق. في نظام الأحداث، يجب إذن تجنب كتابة كود يستحوذ على المعالج لفترة طويلة، لأن الأحداث في هذه الحالة لا تتم معالجتها عند حدوثها، بل في وقت لاحق عندما تصل إليها حلقة الأحداث. ونحصل عندئذ على تطبيق "غير تفاعلي" إلى حد ما؛

يُظهر البرنامج النصي [async-03] مثالاً على هذه الظاهرة:


'use strict';

// يمكن للوظائف غير المتزامنة إرجاع نتيجة عن طريق إصدار حدث
// يمكن للكود الرئيسي استرداد هذه النتائج من خلال الاشتراك في الأحداث التي تم إرسالها

// الاستيرادات
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';

// بداية
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// مرسل أحداث
const eventEmitter = new EventEmitter();

// setTimeout يضبط مؤقتًا لمدة 1000 مللي ثانية (المعلمة الثانية) ويعيد على الفور رقم هذا المؤقت
// عندما ينتهي المؤقت من 1000 مللي ثانية، فإنه يرسل حدثًا يتم وضعه في قائمة انتظار وقت التشغيل
// عندما تتم معالجة الحدث بواسطة وقت التشغيل، يتم تنفيذ الدالة (المعلمة الأولى)
setTimeout(function () {
  // سيتم تنفيذ هذا الرمز عندما يصل المؤقت إلى القيمة 0
  console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
  // يتم إرسال حدث للإشارة إلى توفر نتيجة
  eventEmitter.emit("timer1Success", { success: 4 });
  // يتم إرسال حدث آخر للإشارة إلى توفر نتيجة أخرى
  eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)

// يتم الاشتراك في الحدث [timer1Success]
eventEmitter.on('timer1Success', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});

// يتم الاشتراك في الحدث [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});

// رمز متزامن مكثف بعض الشيء يمنع الرمز الرئيسي من الانتهاء قبل نهاية [timer1]
for (let i = 0; i < 1000000; i++) {
  for (let j = 0; j < 10000; j++) {
    i + i ^ 2 + i ^ 3;
  }
}

// سيتم عرضه قبل رسائل الأحداث الصادرة عن الوظيفة المرتبطة بـ [timer1]
console.log("[fin du script],", heure(débutScript));

// أداة عرض الوقت والمدة
function heure(début) {
 ...
}

تعليقات

  • هذا الكود هو كود المثال السابق [async-02] الذي أضيفت إليه الأسطر 39-44؛
  • الأسطر 20-27: تمت برمجة الدالة [setTimeout] لتنفيذ دالة داخلية غير متزامنة بعد مرور ثانية واحدة. بعد مرور هذه الثانية، لا يتم تنفيذ الدالة غير المتزامنة للمؤقت على الفور: يتم وضع حدث في حلقة التنفيذ لطلب ذلك. إذا كانت حلقة التنفيذ مشغولة بمعالجة حدث آخر، فسيتعين على تنفيذ الدالة غير المتزامنة للمؤقت الانتظار؛
  • الأسطر 20-27: بمجرد أن تقوم الدالة [setTimeout] بضبط مؤقتها على مهلة مدتها ثانية واحدة، فإنها تترك المعالج وتعيد السيطرة إلى الكود المستدعي. ويستمر هذا الكود مع الأسطر 30-37 التي تمثل اشتراكات في الأحداث والتي لها وقت تنفيذ ضئيل؛
  • يستمر الكود الرئيسي بالسطور 40-44 التي تشكل حلقة من 1010 تكرارات. سيكون هذا الكود قيد التنفيذ عندما يصدر المؤقت حدث "نهاية مهلة ثانية واحدة". يُوضع هذا الحدث بعد ذلك في حلقة الأحداث ولكنه سيضطر إلى الانتظار حتى نهاية تنفيذ الكود الرئيسي للنص البرمجي ليكون له فرصة في المعالجة؛
  • السطر 47: نهاية الكود الرئيسي للبرنامج النصي. بعد هذا العرض الأخير، سيتم معالجة حدث انتهاء المؤقت وستتمكن الدالة غير المتزامنة الداخلية في [setTimeout] من التنفيذ؛

يعطي البرنامج النصي النتائج التالية:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-03.js"
[début du script], heure=08:55:02:665
[fin du code principal du script], heure=08:55:11:789, durée= 9 seconde(s) et 131 millisecondes
[setTimeout, fin du timer d'1 s], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes, via l'événement [timer1Failure]

[Done] exited with code=0 in 9.796 seconds

تعليقات

  • السطر 3: نرى أن تنفيذ الكود الرئيسي للبرنامج النصي استغرق 9 ثوانٍ. تم تعليق الأحداث التي قد تكون وقعت خلال هذه الفترة في حلقة الأحداث؛
  • السطر 4: نرى أن الحدث [fin du timer] تمت معالجته بعد 5 مللي ثانية من انتهاء الكود الرئيسي. وقد تم إصداره بعد حوالي ثانية واحدة من بدء البرنامج النصي، لكنه اضطر إلى الانتظار 8 ثوانٍ إضافية حتى تمت معالجته أخيرًا؛

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

11.4. البرنامج النصي [async-04]

يُظهر البرنامج النصي [async-04] آلية أخرى، تُسمى [Promise]، وهي وعد بالنتيجة. تتيح هذه الآلية تجنب إدارة أحداث [node.js] بشكل صريح. يتم ذلك ضمنيًا، وبذلك يمكن للمطور تجاهل وجود هذه الأحداث. ومع ذلك، فإن فهمها سيسمح له بفهم أفضل لكيفية عمل [Promise] التي تبدو معقدة للوهلة الأولى.

النوع [Promise] هو فئة جافا سكريبت. يقبل منشئها كمعلمة دالة غير متزامنة تمرر إليها معلمتين يُطلق عليهما تقليديًا [resolve] و [reject]. يمكن أن تحملان اسمًا آخر؛

1
2
3
4
5
6
7
const promise=new Promise(function(resolve, reject){
     // يتم تشغيل مهمة غير متزامنة
    
     // في حالة النجاح: استدعاء resolve(result) حيث [result] هي نتيجة المهمة غير المتزامنة؛
     // في حالة الفشل: استدعاء reject(error) حيث [error] هو كائن يغلف الخطأ الذي تمت مواجهته؛
}
// الاشتراك في الأحداث الصادرة عن المهمة غير المتزامنة لـ [Promise]
  • يقوم منشئ [Promise] بأمرين:
    • إنه ينشئ حدثًا لبدء تنفيذ الدالة [function(resolve, reject)] التي تم تمريرها إليه كمعلمة، لكنه لا ينتظر نتيجتها ويقوم على الفور بإرجاع كائن [Promise] إلى الكود المستدعي. وقد يكون لهذا الكائن أربع حالات:
      • [pending]: العملية غير المتزامنة التي أدت إلى ظهور [Promise] لم تنتهِ بعد؛
      • [fulfilled]: انتهت العملية غير المتزامنة التي أنتجت [Promise] بنجاح؛
      • [rejected]: انتهت العملية غير المتزامنة التي أنتجت [Promise] بالفشل؛
      • [settled]: انتهت العملية غير المتزامنة التي أنتجت [Promise]؛

عندما يعرض المُنشئ نتيجته، يكون الكائن [Promise] الذي تم إنشاؤه في الحالة [pending]، في انتظار نتائج الدالة غير المتزامنة؛

  • (تابع)
    • يتم تشغيل المهمة غير المتزامنة في الأسطر 2-5 على الفور. غالبًا ما تكون المهام غير المتزامنة مهام إدخال/إخراج غير متزامنة تنقسم على النحو التالي:
      1. تنفيذ كود متزامن لبدء عملية الإدخال/الإخراج مع وحدة أخرى، مثل خادم بعيد؛
      2. انتظار استجابة هذا الجهاز؛
      3. معالجة هذا الرد؛

المرحلة 2، وهي انتظار الجهاز الخارجي عن المعالج، هي الأكثر تكلفة. بدلاً من الانتظار:

  • سيتم الإشارة إلى استلام البيانات المطلوبة من الجهاز الخارجي بواسطة حدث؛
  • في الكود المتزامن الذي سيتبع المرحلة 1 (السطر 7 من الكود المثال)، سنشترك في هذا الحدث ثم نعود في وقت ما إلى حلقة أحداث [node.js]. سيتم عندئذ معالجة الحدث التالي في قائمة الأحداث المعلقة؛
  • خلال المرحلة 2، يوجد توازي في التنفيذ ولكن على أجهزة مختلفة:
    • المعالج لدورة الأحداث؛
    • جهاز خارجي (قرص، قاعدة بيانات، خادم بعيد) للبحث عن البيانات المطلوبة؛
  • في نهاية المرحلة 2، عندما تحصل عملية الإدخال/الإخراج على البيانات المطلوبة، يتم إصدار حدث للإشارة إلى أن نتيجة الإدخال/الإخراج متاحة. ينضم هذا الحدث بعد ذلك إلى الأحداث الأخرى في قائمة انتظار الأحداث؛
  • وعندما يحين دوره، سيتم معالجته. وعندئذٍ سيتم تنفيذ الوظيفة المرتبطة بهذا الحدث (السطر 7 من كود المثال)؛

يسمح هذا النمط من التشغيل بتجنب فترات التوقف: تلك التي ينتظر فيها المعالج استجابة جهاز أبطأ منه؛

  • (تابع)
    • عندما يتم تشغيل المهمة غير المتزامنة في السطرين 2 و5 وتنتهي من عملها، يمكنها إرجاع نتيجة إلى الكود المستدعي، بفضل الدالتين [resolve, reject] اللتين مررهما لها المُنشئ [Promise] كمعلمات. الاتفاقية هي كما يلي:
      • تشير المهمة غير المتزامنة إلى النجاح بواسطة [resolve(result)]. وهذا يعادل وضع حدث في حلقة أحداث [node.js]، يمكن تسميته [resolved] مع [result] كبيانات مرتبطة؛
      • تشير المهمة غير المتزامنة إلى فشل بواسطة [reject(error)]. وهذا يعادل وضع حدث يمكن تسميته [rejected] في حلقة أحداث [node.js]، مع [error] كبيانات مرتبطة به، وعادةً ما يكون كائنًا يوضح تفاصيل الخطأ الذي حدث؛
      • لذلك يجب أن يشترك الكود المستدعي في هذين الحدثين ليتم إخطاره بتوفر نتيجة الدالة غير المتزامنة؛

بعد الانتهاء من تنفيذ المهمة غير المتزامنة المُغلفة في [Promise]، يتغير حالة الكائن [promise] الذي تم إنشاؤه بواسطة المُنشئ [Promise(…)]:

  • يؤدي الحدث [resolved] إلى تغيير حالته من [pending] إلى [resolved]؛
  • يؤدي الحدث [rejected] إلى انتقاله من الحالة [pending] إلى [rejected]؛

يتم الاشتراك في الأحداث [resolved] و [rejected] للمهمة غير المتزامنة باستخدام طرق من الفئة [Promise] بالصيغة التالية:

promise.then(f1).catch(f2).finally(f3) ;

حيث:

  • f1 هي دالة يتم تنفيذها عندما يتغير حالة [promise] من [pending] إلى [resolved]، أي عندما تنجح المهمة غير المتزامنة في إنجاز عملها. وهي تتلقى كمعلمة القيمة [result]، التي يتم إرسالها بواسطة الأمر [resolve(result)] الخاص بالمهمة غير المتزامنة؛
  • f2 هي دالة يتم تنفيذها عندما يتغير حالة [promise] من [pending] إلى [rejected]، أي عندما تفشل المهمة غير المتزامنة في إنجاز عملها. وهي تتلقى كمعلمة القيمة [error]، التي يتم تمريرها بواسطة الأمر [reject(errror)] الخاص بالمهمة غير المتزامنة؛
  • f3 هي دالة يتم تنفيذها بعد تنفيذ الطرق [then] أو [catch]، وبالتالي يتم تنفيذها دائمًا. ولا تتلقى أي معلمة؛

تخفي هذه الصيغة تمامًا الأحداث التي يتم الاشتراك فيها. ومع ذلك، فهي اشتراك، ومثل الاشتراك في المثال السابق، لا تنفذ الدالة [f1, f2, f3] على الفور. سيتم تنفيذ هذه الدالة أو عدم تنفيذها عندما يحدث أحد الأحداث [resolved, rejected] التي تم الاشتراك فيها.

يوضح البرنامج النصي [async-04] هذه الآلية:


'use strict';

// من الممكن الحصول على نتائج (نجاح، فشل) دالة غير متزامنة
// دون استخدام الأحداث بشكل صريح بفضل الفئة [Promise]
// تستخدم هذه الفئة الأحداث ضمناً ولكنها لا تظهر في الكود

// الاستيرادات
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// البداية
const débutScript = moment(Date.now());
console.log("[début du script],", heure(débutScript));

// تعريف مهمة غير متزامنة باستخدام وعد [Promise]
// المهمة غير المتزامنة هي معلمة المنشئ [Promise]
const débutPromise1 = moment(Date.now());
const promise1 = new Promise(function (resolve) {
  // سجل
  console.log("[début fonction asynchrone de promise1],", heure(débutPromise1));
  // كود غير متزامن
  setTimeout(function () {
    console.log("[fin fonction asynchrone de promise1],", heure(débutPromise1));
    // المهمة غير المتزامنة تُرجع نتيجة باستخدام الدالة [resolve]
    // يتم عندئذٍ الوفاء بالوعد
    resolve('[réussite]');
  }, 1000)
});

// يمكن معرفة نتيجة الوعد [promise1]
// عندما يتم حلها (resolve) أو رفضها (reject)
// التعليمات التالية هي اشتراك في الحدث [resolved] عبر الطريقة [then]
// وإلى الحدث [rejected] عبر الطريقة [catch]
// يتم تنفيذ الطريقة [finally] سواء بعد then أو catch
promise1.then(result => {
  // في حالة نجاح الوعد  [evt resolved]
  console.log(sprintf("[promise1.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
  // حالة الخطأ  [evt rejected]
  console.log(sprintf("[promise1.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
  // يتم تنفيذه في جميع الحالات
  console.log("[promise1.finally]", heure(débutPromise1));
});

// تعريف مهمة غير متزامنة باستخدام وعد [Promise]
const débutPromise2 = moment(Date.now());
const promise2 = new Promise(function (resolve, reject) {
  // سجل
  console.log("[début fonction asynchrone de promise2],", heure(débutPromise1));
  // مهمة غير متزامنة
  setTimeout(function () {
    console.log("[fin fonction asynchrone de promise2],", heure(débutPromise2));
    // المهمة غير المتزامنة تُرجع نتيجة باستخدام الدالة [reject]
    // الوعد فشل إذن
    reject('[échec]');
  }, 2000)
});

// يمكن معرفة نتيجة الوعد [promise2]
// عندما يتم حلها (resolve) أو رفضها (reject)
promise2.then(result => {
  // في حالة نجاح الوعد [evt resolved]
  console.log(sprintf("[promise2.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
  // في حالة حدوث خطأ [evt rejected]
  console.log(sprintf("[promise2.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
  // يتم تنفيذه في جميع الحالات
  console.log(sprintf("[promise2.finally], %s", heure(débutPromise2)));
});

// سيظهر قبل رسائل الوظائف غير المتزامنة وتلك الخاصة بالأحداث المرتبطة
console.log("[fin du code principal du script],", heure(débutScript));

// أداة مساعدة
function heure(début) {
  // الوقت الحالي
  const now = moment(Date.now());
  // تنسيق الوقت
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // تنسيق المدة
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // النتيجة
  return result;
}

تعليقات

  • الأسطر 18-28: إنشاء [Promise promise1]. تعرض وظيفتها غير المتزامنة النتيجة عبر حدث بعد ثانية واحدة. بمجرد بدء هذه العملية غير المتزامنة (تشغيل مؤقت)، لا ننتظر حتى تعرض النتيجة وننتقل فورًا إلى كود السطر 35؛
  • الأسطر 35-44: الاشتراك في الحدثين [resolved, rejected] اللذين يمكن أن تصدرهما الوظيفة غير المتزامنة الداخلية لـ [promise1]؛
  • الأسطر 46-71: نكرر نفس تسلسل الكود السابق لوعود ثانية [promise2]؛
  • السطر 74: انتهى الكود الرئيسي للنص البرمجي ولكن لم ينته النص البرمجي ككل لأن عمليتين غير متزامنتين قد تم إطلاقهما. نعود إلى حلقة الأحداث حيث سيحدث في وقت ما أحد الأحداث [resolved, rejected] للوعود [promise1, promise2]. سيتم معالجته عندئذٍ؛
  • ثم يتم العودة إلى حلقة الأحداث. وهناك سيتم معالجة الحدث الثاني [resolved, rejected] من الوعود [promise1, promise2] عندما يحدث؛

التنفيذ


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-04.js"
[début du script], heure=09:39:05:950, durée= 0 seconde(s) et 3 millisecondes
[début fonction asynchrone de promise1], heure=09:39:05:958, durée= 0 seconde(s) et 0 millisecondes
[début fonction asynchrone de promise2], heure=09:39:05:959, durée= 0 seconde(s) et 1 millisecondes
[fin du code principal du script], heure=09:39:05:960, durée= 0 seconde(s) et 13 millisecondes
[fin fonction asynchrone de promise1], heure=09:39:06:977, durée= 1 seconde(s) et 19 millisecondes
[promise1.then], heure=09:39:06:980, durée= 1 seconde(s) et 21 millisecondes, result=[réussite]
[promise1.finally] heure=09:39:06:982, durée= 1 seconde(s) et 24 millisecondes
[fin fonction asynchrone de promise2], heure=09:39:07:976, durée= 2 seconde(s) et 17 millisecondes
[promise2.catch], heure=09:39:07:978, durée= 2 seconde(s) et 19 millisecondes, result=[échec]
[promise2.finally], heure=09:39:07:980, durée= 2 seconde(s) et 21 millisecondes

[Done] exited with code=0 in 2.589 seconds

تعليقات

  • السطر 3: يتم تشغيل الدالة غير المتزامنة لـ [promise1] ولكن لا يتم انتظار انتهائها الذي سيتم الإبلاغ عنه بواسطة حدث؛
  • السطر 4: يتم تشغيل الدالة غير المتزامنة لـ [promise2] ولكن لا يتم انتظار انتهائها الذي سيتم الإبلاغ عنه بواسطة حدث؛
  • السطر 5: نهاية الكود الرئيسي والعودة إلى حلقة الأحداث؛
  • السطر 6: معالجة الحدث [fin fonction asynchrone de promise1]. سيتحول حالة [promise1] إلى [resolved]. ويشير إلى ذلك حدث؛
  • السطر 7: بما أن [promise2] لم تنته بعد من عملها، سيتم معالجة الحدث [promise1 resolved] الذي تم إدراجه للتو في الحلقة بواسطة الطريقة [promise1.then] ثم بواسطة الطريقة [promise.finally] (السطر 8)؛
  • السطور 9-11: تحدث الآلية نفسها عندما ينتقل [promise2] من الحالة [pending] إلى [resolved]؛

11.5. نص برمجي [async-05]

لنعد إلى كود منشئ كائن [Promise]:

1
2
3
4
5
6
7
8
9
'use strict'; 

const promise=new Promise(function(resolve, reject){
// يتم تشغيل مهمة غير متزامنة
// …
// في حالة النجاح: استدعاء resolve(result) حيث [result] هي نتيجة المهمة غير المتزامنة؛
// في حالة الفشل: استدعاء reject(error) حيث [error] هو كائن يغلف الخطأ الذي تمت مواجهته؛
}
// الاشتراك في الأحداث الصادرة عن المهمة غير المتزامنة

في السطر 2، يتم تشغيل المهمة غير المتزامنة لـ [Promise]. غالبًا ما تحتاج هذه المهمة إلى معلمات أكثر من معلمات [resolve, reject] التي تمررها إليها الدالة التي تغلفها. في هذه الحالة، يتم تغليف إنشاء [Promise] في دالة ستمرر لها المعلمات التي تحتاجها وظيفتها غير المتزامنة:

'use strict';

// تعريف الدالة غير المتزامنة
function uneFonctionAsynchrone (p1, p2, , pn){
 return new Promise(function(resolve, reject){
     // يتم تشغيل مهمة غير متزامنة مع المعلمات (P1, p2, …, pn)
    // …
     // في حالة النجاح: استدعاء resolve(result) حيث [result] هي نتيجة المهمة غير المتزامنة؛
     // في حالة الفشل: استدعاء reject(error) حيث [error] هو كائن يغلف الخطأ الذي تمت مواجهته؛
}
// نقومعلى أحداث [resolved, rejected] التي ستصدرها الدالة غير المتزامنة [uneFonctionAsynchrone]

// بعد فترة، يتم استدعاء الدالة غير المتزامنة [uneFonctionAsynchrone]
uneFonctionAsynchrone(e1, e2, , en) ;

النص البرمجي التالي:

  • يحدد وظيفتين غير متزامنتين تعيدان [Promise
  • يبدأ تنفيذهما بالتوازي وينتظر حتى تنتهي الاثنتان للقيام بعمل معين؛

'use strict';

// يمكن تعريف وظائف غير متزامنة ترجع نوعًا [Promise]
// ويمكن عندئذٍ تمييزها بالكلمة الرئيسية [async]

// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// بداية
const débutScript = moment(Date.now());
console.log("[début du script],", heure());

// وظيفة غير متزامنة تعيد وعدًا [Promise]
function async01(p1) {
  return new Promise((resolve) => {
    console.log("[début de la tâche asynchrone async01]");
    // المهمة غير المتزامنة
    const débutAsync01 = moment(Date.now());
    setTimeout(function () {
      console.log("[fin de la tâche asynchrone async01],", heure(débutAsync01));
      // المهمة غير المتزامنة يمكن أن تعطي نتيجة معقدة
      resolve({
        prop1: [10, 20, 30],
        prop2: "abcd",
        prop3: p1,
      });
    }, 1000)
  });
}

// وظيفة غير متزامنة تُرجع وعدًا [Promise]
function async02(p1, p2) {
  return new Promise(resolve => {
    console.log("[début de la tâche asynchrone async02]");
    // مهمة غير متزامنة
    const débutAsync02 = moment(Date.now());
    setTimeout(function () {
      console.log("[fin de la tâche asynchrone async02],", heure(débutAsync02));
      // المهمة غير المتزامنة قد تعطي نتيجة معقدة
      resolve({
        prop1: [11, 21, 31],
        prop2: "xyzt",
        prop3: p1 + p2
      });
    }, 2000)
  })
}

// يتم تشغيل الدالتين غير المتزامنتين بالتوازي
// وننتظر حتى تنتهي كلتاهما
// لا يتم تنفيذ then إلا إذا أصدرت كلتا الدالتين الحدث [resolved]
// يتم تنفيذ catch بمجرد أن تصدر إحدى الدالتين الحدث [rejected]
Promise.all([async01(10), async02(10, 20)])
  // والنتيجة هي مصفوفة [result1, result2] حيث [result1] هي النتيجة الصادرة عن [resolve] من [async01]
  // و [result2] هي النتيجة الصادرة عن [resolve] من [async02]
  .then(result => {
    console.log(sprintf("[promise-all success], %s, result=%j", heure(débutScript), result));
  })
  // error هي النتيجة الصادرة عن أول [reject] لإحدى الدالتين غير المتزامنتين
  .catch(error => {
    console.log(sprintf("[promise-all error], %s, erreur=%j", heure(débutScript), error));
  })
  // finally بعد then أو catch
  .finally(() => {
    console.log(sprintf("[promise-all finally], %s", heure(débutScript)));
  });

// سيظهر قبل رسائل الوظائف غير المتزامنة والأحداث المرتبطة بها
console.log("[fin du code principal du script],", heure(débutScript));

// أداة مساعدة
function heure(début) {
  // الوقت الحالي
  const now = moment(Date.now());
  // تنسيق الوقت
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // تنسيق المدة
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // النتيجة
  return result;
}

تعليقات

  • الأسطر 15-30: يتم تعريف دالة [async01] التي تعرض نتيجتها بعد ثانية واحدة عبر حدث مؤقت. الدالة [async01] التي يتم استخدامها في نتيجتها في السطر 26؛
  • الأسطر 33-47: نفعل الشيء نفسه مع دالة [async02] التي تعرض النتيجة بعد 2 ثانية عبر حدث مؤقت. الدالة [async02] تقبل معلمتين يتم استخدامهما في النتيجة في السطر 44؛
  • عند استدعاء الدالتين [async01, async02]:
    • سيتم تشغيلهما؛
    • ستقدمان للرمز المستدعي وعدين [promise1, promise2]؛
    • ثم يعود التنفيذ إلى الكود المستدعي الذي سيواصل مساره؛
    • بعد حوالي ثانية واحدة، ستصدر [async01] حدثًا للإشارة إلى أنها قد أنهت عملها. سيتم وضع الحدث المعني في قائمة الانتظار في حلقة الأحداث المرتبطة بالنتيجة التي أرسلتها [async01] مع الحدث؛
    • بعد حوالي ثانيتين، ستحدث نفس العملية لـ [async02]؛
  • السطر 54: الآن فقط يتم تنفيذ الدوال غير المتزامنة [async01, async02] (الرموز async01(10) و async02(10,20)). يتم تنفيذها داخل مصفوفة تم تمريرها كمعلمة إلى الدالة [Promise.all]. من المعروف أن [async01, async02] تقدم كلاهما وعدًا إلى الكود المستدعي. لذا فإن معلمة [Promise.all] هي مصفوفة من وعدين؛
  • [Promise.all([promise1, promise2, …, promisen]).then(f1).catch(f2).finally(f3)] هو اشتراك في الأحداث:
    • [Promise.all] هو من النوع [Promise]؛
    • سيتم تنفيذ الدالة [f1] الخاصة بالطريقة [then] عندما تنتقل جميع الوعود [promise1, promise2, …, promisen] الموجودة في مصفوفة معلمات الطريقة [all] منالحالة [pending] إلى الحالة [resolved]. بعبارة أخرى، سيتم تنفيذ [f1] عندما تنتهي جميع الوعود في المصفوفة بنجاح؛
    • سيتم تنفيذ الدالة [f2] الخاصة بالطريقة [catch] بمجرد انتقال أحد الوعود في المصفوفة من الحالة [pending] إلى الحالة [rejected]. بعبارة أخرى، يتم تنفيذ [f2] بمجرد فشل أحد الوعود في المصفوفة؛
    • سيتم تنفيذ الدالة [f3] الخاصة بالطريقة [finally] بعد تنفيذ إحدى الطرق [then, catch]، وبالتالي يتم تنفيذها دائمًا؛

يؤدي تنفيذ الكود إلى النتائج التالية:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-05.js"
[début du script], heure=12:17:17:367
[début de la tâche asynchrone async01]
[début de la tâche asynchrone async02]
[fin du code principal du script], heure=12:17:17:375, durée= 0 seconde(s) et 10 millisecondes
[fin de la tâche asynchrone async01], heure=12:17:18:391, durée= 1 seconde(s) et 17 millisecondes
[fin de la tâche asynchrone async02], heure=12:17:19:389, durée= 2 seconde(s) et 14 millisecondes
[promise-all success], heure=12:17:19:390, durée= 2 seconde(s) et 25 millisecondes, result=[{"prop1":[10,20,30],"prop2":"abcd","prop3":10},{"prop1":[11,21,31],"prop2":"xyzt","prop3":30}]
[promise-all finally], heure=12:17:19:392, durée= 2 seconde(s) et 27 millisecondes

[Done] exited with code=0 in 2.572 seconds
  • السطران 6-7: يتم تشغيل المهمتين غير المتزامنتين [async01, async02]. تعملان بالتوازي. لا يتم تنفيذ كودهما بالتوازي، ولكن انتظارهما للبيانات المطلوبة يحدث في نفس الوقت؛
  • السطر 5: تم الانتهاء من الكود الرئيسي للنص البرمجي. يبقى انتظار انتهاء المهمتين غير المتزامنتين [async01, async02]؛
  • السطر 6: تنتهي المهمة غير المتزامنة [async01] بعد حوالي ثانية واحدة من إطلاقها. وهي تُرجع نتيجة باستخدام الدالة [resolve]، وبالتالي فإن وعدها في الجدول في السطر 56 من الكود ينتقل من الحالة [pending] إلى [resolved]. وهذا لا يكفي لتشغيل الطريقة [then]، السطور 59-60 من الكود؛
  • السطر 7: تنتهي المهمة غير المتزامنة [async02] بعد حوالي ثانيتين من بدء تشغيلها. وهي تُرجع نتيجة باستخدام الدالة [resolve]، وبالتالي فإن وعدها في الجدول في السطر 56 من الكود ينتقل من الحالة [pending] إلى [resolved]. سيتم تنفيذ الطريقة [then] بمجرد أن تسمح بذلك حلقة الأحداث؛
  • السطر 8: يتم تنفيذ الطريقة [then] من [Promise.all]. وهي تتلقى كمعلمة مصفوفة [result1, result2] حيث [result1] هي النتيجة الصادرة عن [async01]، و [result2] هي النتيجة الصادرة عن [async02]؛
  • السطر 9: يتم تنفيذ الأسلوب [finally] الخاص بـ [Promise.all]؛

11.6. نص برمجي [async-06]

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

نستأنف المثال السابق بإجراء التعديلات التالية عليه:

  • نضيف دالة غير متزامنة ثالثة [async03] والتي تعيد النتيجة باستخدام الطريقة [Promise.reject] التي تشير بالتالي إلى حلقة الأحداث بأنها "فشلت" في أداء مهمتها؛
  • نقوم بتنفيذ الوظائف غير المتزامنة الثلاث [async01, async02, async03] بالتسلسل. في المثال السابق، كنا قد نفذنا الوظائف غير المتزامنة [async01, async02] بالتوازي؛
  • قبل ظهور الكلمات الرئيسية [async/await]، كان التنفيذ التسلسلي للأعمال غير المتزامنة يتم باستخدام [Promise] المتداخلة مع بعضها البعض. وبمجرد وجود عدة إجراءات غير متزامنة لتنفيذها بهذه الطريقة، كان عدد الوعود يزداد تبعًا لذلك وكان الكود يصبح أقل قابلية للقراءة؛
  • مع الكلمات الرئيسية [async/await]، يتم تنفيذ المهام غير المتزامنة بالتسلسل باستخدام صيغة مشابهة لتلك المستخدمة في تنفيذ المهام المتزامنة:

// وظيفة غير متزامنة - استخدام async / await
async function main() {
  // التنفيذ التسلسلي للمهام غير المتزامنة
  try {
    // التنفيذ مع انتظار [async01]
    const result1 = await async01(...);
    console.log("[async01 result]=", result1);
    // التنفيذ مع انتظار [async02]
    const result2 = await async02(...);
    console.log("[async02 result]=", result2);
    // التنفيذ مع انتظار [async03]
    const result3 = await async03(...);
    console.log("[async03 result]=", result3);
  } catch (error) {
    // فشل أحد الإجراءات غير المتزامنة
    console.log(sprintf("[sequential error]= %j, %s", error));
  } finally {
    // انتهى
    console.log("[fin exécution séquentielle des tâches asynchrones],");
  }
  • السطر 6: يتم تشغيل الدالة غير المتزامنة [async01] (الكلمة الرئيسية await) وننتظر حتى تنشر نتيجتها عبر إحدى الطرق [Promise.resolve, Promise.reject]. إنها إذن عملية معطلة؛
  • السطر 6: الكلمة الرئيسية [await] تحول العملية غير المتزامنة [async01] إلى عملية معطلة. نعلم أن العملية [async01] تُرجع نتيجة بطريقتين:
    • تُرجع إلى الكود المستدعي، بشكل شبه فوري، كائن [Promise]؛
    • تنشر لاحقًا نتيجة على حلقة الأحداث عبر الطرق [Promise.resolve, Promise.reject]. وهذه النتيجة الأخيرة هي التي يستردها [result1]، السطر 6. أصبحت إدارة الأحداث لعملية [async01] غير مرئية؛
    • إذا تم نشر النتيجة [result] لـ [async01] بواسطة [Promise.resolve(result)]، يتم تعيينها إلى [result1] في السطر 6 ويستمر التنفيذ في السطر 7؛
    • إذا تم نشر نتيجة [async01] بواسطة [Promise.reject]، فإن ذلك يؤدي إلى حدوث استثناء وينتقل تنفيذ الكود إلى السطر 14، وهو سطر catch. معلمة جملة [catch] هي كائن الخطأ (error) الذي تم نشره بواسطة [async01] مع تعبير [Promise.reject(error)]. يمكن للمهمة غير المتزامنة أيضًا نشر الخطأ عبر [throw(error)]. الكائن [error] هو الكائن الذي تم استرداده في [catch(error)]؛
    • يجب أن تكون الكلمة الرئيسية [await] موجودة بالضرورة في دالة مسبوقة بالكلمة الرئيسية [async]، السطر 2. تشير هذه الكلمة الرئيسية إلى أن الدالة [main] هي دالة غير متزامنة؛
    • في التعبير [await f(…)]، يجب أن تكون [f] دالة غير متزامنة تعيد كائن [Promise] إلى الكود المستدعي؛
  • نكرر نفس الشيء بالنسبة للإجراء غير المتزامن [async02]، السطر 9 و [async03]، السطر 12؛

باستخدام الكلمات الرئيسية [async / await]، يمكن تنفيذ المهام غير المتزامنة بشكل متوازٍ باستخدام الصيغة التالية:


try {
    // التنفيذ المتوازي للمهام غير المتزامنة
    const result = await Promise.all([async01(...), async02(...), async03(...)]);
    console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
  } catch (error) {
    // فشل إحدى الإجراءات غير المتزامنة
    console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
  } finally {
    // انتهى
    console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
}
  • السطر 3: لدينا عملية معطلة: ننتظر حتى تنشر المهام الثلاث غير المتزامنة في المصفوفة [async01(..), async02(..), async03(..)] نتائجها على حلقة الأحداث باستخدام إحدى الطرق [Promise.resolve, Promise.reject]؛
  • إذا نشرت المهام الثلاث غير المتزامنة نتائجها باستخدام [Promise.resolve]، فإن الثابت [result] يكون عندئذٍ المصفوفة [result1, result2, result3] حيث:
    • [result1] هي النتيجة التي نشرتها [async01] باستخدام التعبير [Promise.resolve(result1)]؛
    • [result2] هي النتيجة التي نشرها [async02] باستخدام التعبير [Promise.resolve(result2)]؛
    • [result3] هي النتيجة التي نشرها [async03] باستخدام التعبير [Promise.resolve(result3)]؛
  • إذا نشرت إحدى المهام الثلاث نتيجتها باستخدام التعبير [Promise.reject(error)]، فسيحدث استثناء؛
    • لا تتلقى الثابتة [result] في السطر 3 قيمتها؛
    • ينتقل التنفيذ مباشرة إلى [catch] في السطر 5؛
    • المعلمة (error) في catch، هي الكائن (error) الذي تم نشره بواسطة التعبير [Promise.reject(error)]؛

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

فيما يلي كود البرنامج النصي [async-06] الكامل:


'use strict';

// التنفيذ المتوازي أو التسلسلي لعدة مهام غير متزامنة
// باستخدام الكلمات الرئيسية async / await

// استيرادات
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// بداية
const débutScript = moment(Date.now());
console.log("[début du code principal du script],", heure());

// دالة غير متزامنة تُرجع [Promise]
function async01(débutAsync01) {
  return new Promise(function (resolve) {
    console.log("[début fonction asynchrone async01],", heure());
    // وظيفة غير متزامنة
    setTimeout(function () {
      console.log("[fin fonction asynchrone async01],", heure(débutAsync01));
      // قد تعطي العملية غير المتزامنة نتيجة معقدة
      // نجاح هنا
      resolve({
        prop1: [11, 21, 31],
        prop2: "abcd"
      });
    }, 1000)
  });
}

// وظيفة غير متزامنة تعطي [Promise]
function async02(débutAsync02) {
  console.log("[début fonction asynchrone async02],", heure());
  return new Promise(function (resolve) {
    // وظيفة غير متزامنة
    setTimeout(function () {
      console.log("[fin fonction asynchrone async02],", heure(débutAsync02));
      // قد تؤدي العملية غير المتزامنة إلى نتيجة معقدة
      // نجاح هنا
      resolve({
        prop1: [12, 22, 32],
        prop2: "xyzt"
      });
    }, 2000)
  })
}

// وظيفة غير متزامنة تعطي [Promise]
function async03(débutAsync03) {
  console.log("[début fonction asynchrone async03],", heure());
  return new Promise((resolve, reject) => {
    // وظيفة غير متزامنة
    setTimeout(function () {
      console.log("[fin fonction asynchrone async03],", heure(débutAsync03));
      // قد تؤدي العملية غير المتزامنة إلى نتيجة معقدة
      // فشل هنا
      reject({
        prop1: [13, 23, 33],
        prop2: "échec"
      });
    }, 3000)
  })
}

// وظيفة غير متزامنة - استخدام async / await
async function main() {
  const débutSequential = moment(Date.now());
  // التنفيذ التسلسلي للمهام غير المتزامنة
  console.log("------------ exécution séquentielle des tâches asynchrones lancée ------------------------")
  try {
    // التنفيذ مع انتظار [async01]
    const débutAsync01 = moment(Date.now());
    const result1 = await async01(débutAsync01);
    console.log("[async01 result]=", result1);
    // التنفيذ مع انتظار [async02]
    const débutAsync02 = moment(Date.now());
    console.log("début async02-------------", heure());
    const result2 = await async02(débutAsync02);
    console.log("[async02 result]=", result2);
    // التنفيذ مع انتظار [async03]
    const débutAsync03 = moment(Date.now());
    console.log("début async03-------------", heure());
    const result3 = await async03(débutAsync03);
    console.log("[async03 result]=", result3);
  } catch (error) {
    // فشل أحد الإجراءات غير المتزامنة
    console.log(sprintf("[sequential error]= %j, %s", error, heure(débutSequential)));
  } finally {
    // انتهى
    console.log("[fin exécution séquentielle des tâches asynchrones],", heure(débutSequential));
  }

  const débutParallel = moment(Date.now());
  // تنفيذ المهام غير المتزامنة بالتوازي
  console.log("------------ exécution parallèle des tâches asynchrones lancée ------------------------")
  try {
    const result = await Promise.all([async01(débutParallel), async02(débutParallel), async03(débutParallel)]);
    console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
  } catch (error) {
    // فشل إحدى الإجراءات غير المتزامنة
    console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
  } finally {
    // انتهى
    console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
  }

  // انتهى
  console.log("[fin de la fonction main],", heure(débutSequential));
}
// تنفيذ وظيفة غير متزامنة main
main();

// سيظهر قبل الرسائل المختلفة للوظائف غير المتزامنة وأحداثها
console.log("[fin du code principal du script],", heure(débutScript));

// أداة مساعدة
function heure(début) {
  // الوقت الحالي
  const now = moment(Date.now());
  // تنسيق الوقت
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // تنسيق المدة
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // النتيجة
  return result;
}

نتائج التنفيذ هي كما يلي:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-06.js"
[début du code principal du script], heure=15:02:00:152
------------ exécution séquentielle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=15:02:00:161
[fin du code principal du script], heure=15:02:00:164, durée= 0 seconde(s) et 15 millisecondes
[fin fonction asynchrone async01], heure=15:02:01:165, durée= 1 seconde(s) et 4 millisecondes
[async01 result]= { prop1: [ 11, 21, 31 ], prop2: 'abcd' }
début async02------------- heure=15:02:01:253
[début fonction asynchrone async02], heure=15:02:01:254
[fin fonction asynchrone async02], heure=15:02:03:265, durée= 2 seconde(s) et 12 millisecondes
[async02 result]= { prop1: [ 12, 22, 32 ], prop2: 'xyzt' }
début async03------------- heure=15:02:03:268
[début fonction asynchrone async03], heure=15:02:03:268
[fin fonction asynchrone async03], heure=15:02:06:285, durée= 3 seconde(s) et 18 millisecondes
[sequential error]= {"prop1":[13,23,33],"prop2":"échec"}, heure=15:02:06:289, durée= 6 seconde(s) et 129 millisecondes
[fin exécution séquentielle des tâches asynchrones], heure=15:02:06:291, durée= 6 seconde(s) et 131 millisecondes
------------ exécution parallèle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=15:02:06:292
[début fonction asynchrone async02], heure=15:02:06:293
[début fonction asynchrone async03], heure=15:02:06:294
[fin fonction asynchrone async01], heure=15:02:07:294, durée= 1 seconde(s) et 2 millisecondes
[fin fonction asynchrone async02], heure=15:02:08:298, durée= 2 seconde(s) et 6 millisecondes
[fin fonction asynchrone async03], heure=15:02:09:297, durée= 3 seconde(s) et 5 millisecondes
[parallel error], heure=15:02:09:298, durée= 3 seconde(s) et 6 millisecondes, erreur={"prop1":[13,23,33],"prop2":"échec"}
[fin exécution parallèle des tâches asynchrones],heure=15:02:09:299, durée= 3 seconde(s) et 7 millisecondes
[fin de la fonction main], heure=15:02:09:300, durée= 9 seconde(s) et 140 millisecondes

[Done] exited with code=0 in 9.668 seconds