دليل تعلم جافاسكربت المكررات iterators والمولدات generators غير المتزامنة في جافاسكربت


صفا الفليج

تُتيح لنا المُكرّرات غير المتزامنة المرور على البيانات التي تأتينا على نحوٍ غير متزامن متى لزم، مثل حين نُنزّل شيئًا كتلةً كتلةً عبر الشبكة. المولّدات غير المتزامنة تجعل من ذلك أسهل فأسهل.

لنرى مثالًا أولًا لنفهم الصياغة، بعدها نرى مثالًا من الحياة العملية.

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

تتشابه المُكرّرات غير المتزامنة مع تلك العادية، بفروق بسيطة في الصياغة.

فكائن المُكرّر العادي (كما رأينا في الفصل الكائنات المكرَّرة (Iterables) في جافاسكربت) يكون هكذا:

let range = {
  from: 1,
  to: 5,
  // ‫تستدعي حلقة for..of هذه الدالّة مرة واحدة في البداية

  [Symbol.iterator]() {

    // ‫...ستعيد كائن مُكرُر:
    // ‫لاحقًا ستعمل حلقة for..of فقط مع ذلك الكائن.
    //  ‫وتطلب منه القيم التالية باستخدام دالة next()‎
    return {
      current: this.from,
      last: this.to,

      // ‫تُستدعى next()‎ في كلّ تكرار من خلال الحلقة for..of

      next() { // (2)
        // ‫يجب أن تعيد القيم ككائن {done:.., value :...}

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

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

راجِع … لتعرف تفاصيل المُكرّرات العادية، لو لزم.

ولنجعل الكائن مُتكرّرًا غير متزامنًا:

  1. علينا استعمال Symbol.asyncIterator بدل Symbol.iterator.
  2. على next()‎ إعادة وعد.
  3. علينا استعمال حلقة for await (let item of iterable)‎ لتكرار كائن معين.

فلنصنع كائن range مُتكرّرًا (كما أعلاه) ولكن يُعيد القيم بنحوٍ غير متزامن، قيمةً واحدةً كلّ ثانية:

let range = {
  from: 1,
  to: 5,
  // ‫تستدعي حلقة for..of هذه الدالّة مرة واحدة في البداية

  [Symbol.asyncIterator]() { // (1)

    // ‫...ستعيد كائن مُكرُر:
    // ‫لاحقًا ستعمل حلقة for..of فقط مع ذلك الكائن.
    //  ‫وتطلب منه القيم التالية باستخدام دالة next()‎
    return {
      current: this.from,
      last: this.to,
      // ‫تُستدعى next()‎ في كلّ تكرار من خلال الحلقة for..of

      async next() { // (2)
        // ‫يجب أن تعيد القيم ككائن {done:.., value :...}
        // وستُغلّف تلقائيًا في وعد غير متزامن



        // ‫يمكننا استخدام await لتنفيذ أشياء غير متزامنة:
        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
  }


})()

كما ترى فالبنية تشبه المُكرّرات العادية:

  1. ليكون الكائن مُتكرّرًا على نحوٍ غير متزامن، يجب أن يحتوي التابِع Symbol.asyncIterator (لاحِظ (1)).
  2. على التابِع إعادة الكائن وهو يحتوي التابِع next()‎ الذي يُعيد وعدًا (لاحِظ (2)).
  3. ليس على التابِع next()‎ أن يكون غير متزامن async. فيمكن أن يكون تابِعًا عاديًا يُعيد وعدًا، إلّا أنّ async تُتيح لنا استعمال await وهذا أفضل لنا. هنا نؤخّر التنفيذ ثانيةً واحدةً فقط (لاحِظ (3)).
  4. ليحدث التكرار نستعمل for await(let value of range)‎ (لاحِظ (4))، أي نُضيف await قبل الحلقة for. هكذا تستدعي الحلقة range[Symbol.asyncIterator]()‎ مرةً واحدة، وبعدها تابِع next()‎ للقيم التالية.

إليك ورقة صغيرة تغشّ منها:

  المُكرّرات المُكرّرات غير المتزامنة
تابِع الكائن الذي يُقدّم المُكرّر Symbol.iterator Symbol.asyncIterator
قيمة next() المُعادة هي أيّ قيمة وعد Promise
to loop, use for..of for await..of

تحذير: "لا يعمل معامل البقية ... بطريقة غير متزامنة" الميزات التي تتطلب تكرارات منتظمة ومتزامنة، لا تعمل مع تلك المتزامنة.

على سبيل المثال، لن يعمل معامل البقية هنا:

alert( [...range] ); // Error, no Symbol.iterator

هذا أمر طبيعي، لأنه يتوقع العثور على Symbol.iterator، مثل for..of بدون await. ولكن ليس Symbol.asyncIterator.

المولدات غير المتزامنة

كما نعلم فلغة جافاسكربت تدعم المولّدات أيضًا، وهذه المولّدات مُتكرّرة.

فلنتذكّر مولّد السلاسل من الفصل المولِّداتكان يولّد سلسلة من القيم من متغير البداية 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, then 2, then 3, then 4, then 5
}

لا يمكننا في الدوال العادية استعمال await، فعلى كلّ القيم أن تأتي بنحوٍ متزامن وليس ممكنًا وضع أيّ تأخير في حلقة for..of،

ولكن، ماذا لو أردنا استعمال await في المولّد؟ مثلًا لنُرسل الطلبات عبر الشبكة؟

لا مشكلة، نضع قبله async هكذا:

async function* generateSequence(start, end) {

  for (let i = start; i <= end; i++) {


    // yay, can use await!
    await new Promise(resolve => setTimeout(resolve, 1000));


    yield i;
  }

}

(async () => {

  let generator = generateSequence(1, 5);
  for await (let value of generator) {
    alert(value); // 1, then 2, then 3, then 4, then 5
  }

})();

الآن لدينا مولّد غير متزامن، وقابل للتكرار مع for await...of.

إنها حقًا بسيطة للغاية. نضيف الكلمة المفتاحية async، ويستطيع المولّد الآن استخدام await بداخله، ويعتمد على الوعود والدوالّ غير متزامنة الأخرى.

وتقنيًا، الفرق الآخر للمولّد غير المتزامن هو أنّ تابِع generator.next()‎ صار غير متزامنًا أيضًا ويُعيد الوعود.

في المولّدات العادية نستعمل result = generator.next()‎ لنأخذ القيم، بينما في المولّدات غير المتزامنة نُضيف await هكذا:

result = await generator.next(); // result = {value: ..., done: true/false}

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

كما نعلم ليكون الكائن مُتكرّرًا علينا إضافة رمز Symbol.iterator إليه.

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

  [Symbol.iterator]() {
    return <object with next to make range iterable>
  }

}

هناك أسلوب شائع هو إعادة Symbol.iterator لمولّد بدل كائن صِرف يحمل التابِع next كما المثال أعلاه.

لنستذكر معًا المثال من الفصل المولِّدات:

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, then 2, then 3, then 4, then 5
}

نرى الكائن المخصّص range هنا مُتكرّرًا، والمولّد ‎*[Symbol.iterator]‎ يؤدّي المنطق اللازم لسرد القيم.

لو أردنا إضافة أيّ إجراءات غير متزامنة للمولّدة، فعلينا استبدال الرمز Symbol.iterator بالرمز Symbol.asyncIterator غير المتزامن.

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


  async *[Symbol.asyncIterator]() { // ‫مشابه لـِ ‎[Symbol.asyncIterator]: async function*()‎

    for(let value = this.from; value <= this.to; value++) {

      // توقف بين القيم لانتظار شيء ما 
      await new Promise(resolve => setTimeout(resolve, 1000));

      yield value;
    }
  }
};

(async () => {

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

})();

الآن تأتينا القيم متأخرة عن بعضها البعض ثانيةً واحدة.

مثال من الحياة العملية

حتى اللحظة كانت الأمثلة كلّها بسيطة، لنفهم القصة فقط. الآن حان الوقت لنُطالع مثالًا وحالةً من الواقع.

نرى على الإنترنت خدمات كثيرة تقدّم لنا البيانات على صفحات (paginated). فمثلًا حين نطلب قائمة من المستخدمين يُعيد الطلب عددًا تحدّد مسبقًا (مثلًا 100 مستخدم)، أي ”صفحة واحدة“، ويعطينا أيضًا عنوان الصفحة التالية.

هذا النمط شائع جدًا بين المطوّرين، وليس مستخدمًا للمستخدمين فقط بل لكلّ شيء. فمثلًا غِت‌هَب تتيح لك جلب الإيداعات بنفس الطريقة باستعمال الصفحات:

  • عليك إرسال طلب إلى المسار https://api.github.com/repos//commits.
  • يستجيب الخادوم بكائن JSON فيه 30 إيداعًا، ويُعطيك رابطًا يوصلك إلى الصفحة التالية في ترويسة Link.
  • بعدها نستعمل ذلك الرابط للطلبات التالية لنجلب إيداعات أكثر، وهكذا.

ولكن ما نريد هو واجهة برمجية أبسط قليلًا: أي كائنًا مُتكرّرًا فيه الإيداعات كي نمرّ عليها بهذا النحو:

let repo = 'javascript-tutorial/en.javascript.info'; // المستودع الذي فيه الإيداعات التي نريد على غِت‌هَب

for await (let commit of fetchCommits(repo)) {
  // نُعالج كلّ إيداع commit
}

ونريد كتابة الدالة fetchCommits(repo)‎ لتجلب تلك الإيداعات لنا وتؤدّي الطلبات اللازمة متى… لزم. كما ونترك في عهدتها مهمة الصفحات كاملةً. ما سنفعله من جهتنا هو أمر بسيط، for await..of فقط.

باستعمال المولّدات غير المتزامنة فهذه المهمّة تصير سهلة:

async function* fetchCommits(repo) {
  let url = `https://api.github.com/repos/${repo}/commits`;

  while (url) {
    const response = await fetch(url, { // (1)
      headers: {'User-Agent': 'Our script'}, 
    });

    const body = await response.json(); // (2) 

    // (3) 
    let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
    nextPage = nextPage && nextPage[1];

    url = nextPage;

    for(let commit of body) { // (4) 
      yield commit;
    }
  }
}
  1. نستعمل تابِع fetch من المتصفّح لتنزيل العنوان البعيد. يُتيح لنا التابِع تقديم تصاريح الاستيثاق وغيرها من ترويسات مطلوبة. غِت‌هَب يطلب User-Agent.
  2. نحلّل نتيجة الجلب على أنّها كائن JSON، وهذا تابِع آخر خاصّ بالتابِع fetch.
  3. نستلم هكذا عنوان الصفحة التالية من ترويسة Link داخل الردّ. لهذا العنوان تنسيق خاص فنستعمل تعبيرًا نمطيًا لتحليله. يظهر عنوان الصفحة التالية على هذا الشكل: https://api.github.com/repositories/93253246/commits?page=2، وموقع غِت‌هَب هو من يولّده بنفسه.
  4. بعدها نُنتِج كلّ الإيداعات التي استلمناها، ومتى انتهت

مثال على طريقة الاستعمال (تعرض من كتبَ الإيداعات في الطرفيّة):

(async () => {

  let count = 0;

  for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {

    console.log(commit.author.login);

    if (++count == 100) { // لنتوقف عند 100 إيداع
      break;
    }
  }

})();

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

خلاصة

تعمل المُكرّرات والمولّدات العادية كما نريد لو لم تأخذ البيانات التي نحتاج وقتًا حتّى تتولّد.

وحين نتوقّع بأنّ البيانات ستأتي بنحوٍ غير متزامن بينها انقطاعات، فيمكن استعمال البدائل غير المتزامنة مثل for await..of بدل for..of.

الفرق في الصياغة بين المُكرّرات العادية وغير المتزامنة:

  المتكرر المتكرر غير المتزامن
التابِع الذي يُقدّم المُكرّر Symbol.iterator Symbol.asyncIterator
قيمة next()‎ المُعادة هي {value:…, done: true/false} Promise والّتي تعوض لتصبح {value:…, done: true/false}

الفرق في الصياغة بين المولّدات العادية وغير المتزامنة:

  المولدات المولدات غير المتزامنة
التعريف function* async function*
next()‎ القيمة المُعادة هي {value:…, done: true/false} Promise والّتي تعوض لتصبح {value:…, done: true/false}

غالبًا ونحن نعمل على تطوير الوِب نواجه سيولًا كبيرة من البيانات، سيولًا تأتينا قطعًا قطعًا (مثل تنزيل أو رفع الملفات الكبيرة).

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

ترجمة -وبتصرف- للفصل Async iterators and generators من كتاب The JavaScript language





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن