٢٦ مايو ٢٠٢٣

الـgenerators والتكرار الغير متزامن

التكرار اللامتزامن والمولدات

يسمح لنا التكرار اللامتزامن بالتكرار على البيانات التي تأتي بشكل لامتزامن، حسب الطلب. مثل، على سبيل المثال، عندما نقوم بتنزيل شيء تدريجيًا عبر الشبكة. وتجعل المولدات اللامتزامنة ذلك أكثر ملاءمة.

المتكررات الغير متزامنة Async iterators

تذكر المتكررات

دعنا نستدعي الموضوع حول المتكررات.

الفكرة هي أن لدينا كائنًا، مثل range هنا:

let range = {
  from: 1,
  to: 5,
};

… ونريد استخدام حلقة for..of عليه، مثل for(value of range)، للحصول على القيم من 1 إلى 5.

بعبارة أخرى، نريد إضافة “قابلية التكرار” إلى الكائن.

يمكن تنفيذ ذلك باستخدام طريقة خاصة بالاسم Symbol.iterator:

  • يتم استدعاء هذه الطريقة من قبل بناء for..of عند بدء الحلقة، ويجب أن تعيد كائنًا يحتوي على الطريقة next.
  • في كل تكرار، ييتم استدعاء طريقة next() للحصول على القيمة التالية.
  • يجب أن ترجع الطريقة next() قيمة في الشكل {done: true/false, value:<loop value>}، حيث يعني done:true نهاية الحلقة.

هنا تنفيذ للمتكرر range:

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() { // يستدعى مرة واحدة، في بداية for..of
    return {
      current: this.from,
      last: this.to,

      next() { // يستدعى في كل تكرار، للحصول على القيمة التالية
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

for(let value of range) {
  alert(value); // 1 ثم 2، ثم 3، ثم 4،ثم 5
}

إذا كانت هناك أي عدم وضوح، يرجى زيارة الفصل Iterables، حيث يوفر كافة التفاصيل حول المتكررات العادية.

المتكررات الأسنكرونية

التكرار الأسنكروني يُحتاج عندما تأتي القيم بشكل أسنكروني: بعد setTimeout أو نوع آخر من التأخير.

الحالة الأكثر شيوعًا هي أن الكائن يحتاج إلى إجراء طلب شبكة لتسليم القيمة التالية، وسنرى مثالًا حيًا عليه لاحقًا.

لجعل كائن قابلًا للتكرار بشكل أسنكروني:

  1. استخدم Symbol.asyncIterator بدلاً من Symbol.iterator.
  2. يجب أن ترجع التابع next() وعدًا (ليتم الوفاء به بالقيمة التالية).
    • يتعامل الكلمة async مع ذلك، لذلك يمكننا ببساطة جعل next() مُشار إليها بـ async.
  3. لتكرار مثل هذا الكائن، يجب علينا استخدام حلقة for await (let item of iterable)، حيث يتم استخدام كلمة await.

لنبدأ بمثال لجعل كائن range قابلًا للتكرار بشكل أسنكروني، وشبيه بالذي سبق، ولكنه الآن سيعيد القيم بشكل أسنكروني، واحدة في الثانية.

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

let range = {
  from: 1,
  to: 5,

  [Symbol.asyncIterator]() { // (1)
    return {
      current: this.from,
      last: this.to,

      async next() { // (2)

        // ملاحظة: يمكننا استخدام "await" داخل next ويتم التعامل معها باستخدام async:
        await new Promise(resolve => setTimeout(resolve, 1000)); // (3)

        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

(async () => {

  for await (let value of range) { // (4)
    alert(value); // 1,2,3,4,5
  }

})()

كما نرى فإن الشكل والتركيب مثل الـiterators العادية:

  1. لجعل الكائن iterable بشكل غير متزامن، يجب أن يحتوى على الدالة Symbol.asyncIterator (1).
  2. هذه الدالة يجب أن تقوم بإرجاع الكائن باستخدام next() والتى تقوم بإرجاع promise (2).
  3. لا يجب أن تكون الدالة next() عباره عن async ويمكن أن تكون دالة عادية تُرجع promise ولكن الكلمة async تمكننا من استخدام الكلمة await وهذا مناسب. وهنا يمكننا التأخير ثانية (3).
  4. للقيام بالتكرار نستخدم التكرار for await(let value of range) (4) ونضع الكلمة “await” بعد “for”. وهي تستدعي الدالة range[Symbol.asyncIterator]() مرة واحدة وثم الدالة next() من أجل القيم.

هذا جدول صغير يوضح الفروقات:

Iterators Async iterators
الدوال لإنشاء متكرر Symbol.iterator Symbol.asyncIterator
القيمة التى تُرجعها next() أى فيمة Promise
للتكرار، نستخدم for..of for await..of
شكل الإنتشار ... لا تعمل بشكل غير متزامن

الأشياء التي تتطلب iterators عادية ومتزامنة synchronous لا تعمل فى المناطق الغير متزامنة.

على سبيل المثال، لا تعمل الـspread syntax:

alert([...range]); // خطأ، لا يوجد Symbol.iterator

هذا طبيعي، حيث يتوقع العثور على Symbol.iterator وليس Symbol.asyncIterator.

هذا أيضًا الحال بالنسبة لـ for..of: الصيغة بدون await تحتاج إلى Symbol.iterator.

إستدعاء المولدات

دعونا نستدعي المولدات، حيث تسمح بجعل كود التكرار أقصر بكثير. في معظم الأحيان، عندما نريد جعل شيء مكرر، سنستخدم المولدات.

للبساطة، بدون بعض الأشياء المهمة، فإنها “دوال تنتج (تصدر) قيمًا”. لقد تم شرحها بالتفصيل في الفصل المُنشِئات Generators.

تتم وسم المولدات باستخدام function* (لاحظ النجمة) واستخدام yield لإصدار قيمة، ثم يمكننا استخدام for..of للحلقة عليها.

يقوم هذا المثال بتوليد سلسلة من القيم من start إلى end:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

for(let value of generateSequence(1, 5)) {
  alert(value); // 1، ثم 2، ثم 3، ثم 4، ثم 5
}

كما نعلم بالفعل، لجعل كائن مكرر، يجب أن نضيف Symbol.iterator إليه.

let range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    return <object with next to make range iterable>
  }
}

الممارسة الشائعة لـ Symbol.iterator هي إرجاع مولد، حيث يجعل الكود أقصر، كما يمكنك ملاحظته:

let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() { // اختصار لـ [Symbol.iterator]: function*()
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

for(let value of range) {
  alert(value); // 1، ثم 2، ثم 3، ثم 4، ثم 5
}

يرجى الرجوع إلى الفصل المُنشِئات Generators إذا كنت ترغب في المزيد من التفاصيل.

في المولدات العادية، لا يمكننا استخدام await. يجب أن تأتي كافة القيم بنفس المزامنة كما هو مطلوب من بناء الحلقة for..of.

ماذا لو كنا نريد إنشاء قيم بشكل غير متزامن؟ من طلبات الشبكة، على سبيل المثال.

دعونا ننتقل إلى المولدات الغير متزامنة لجعل ذلك ممكناً.

المولدات الغير متزامنة (أخيراً)

لمعظم التطبيقات العملية، عندمانريد جعل كائن يولد سلسلة من القيم بشكل غير متزامن، يمكننا استخدام مولد غير متزامن.

الصيغة بسيطة: نقوم بإضافة async قبل function* وذلك يجعل المولد غير متزامن.

ثم نستخدم for await (...) للتكرار عليه، كما يلي: