العربية

استكشف مساعدي المكرِّرات غير المتزامنة في JavaScript لإحداث ثورة في معالجة التدفقات. تعلم كيفية التعامل بكفاءة مع تدفقات البيانات غير المتزامنة باستخدام map، وfilter، وtake، وdrop، وغيرها.

مساعدو المكرِّرات غير المتزامنة في JavaScript: معالجة قوية للتدفقات للتطبيقات الحديثة

في تطوير JavaScript الحديث، يعد التعامل مع تدفقات البيانات غير المتزامنة متطلبًا شائعًا. سواء كنت تجلب البيانات من واجهة برمجة تطبيقات (API)، أو تعالج ملفات كبيرة، أو تتعامل مع أحداث في الوقت الفعلي، فإن إدارة البيانات غير المتزامنة بكفاءة أمر بالغ الأهمية. توفر مساعدو المكرِّرات غير المتزامنة في JavaScript طريقة قوية وأنيقة لمعالجة هذه التدفقات، مقدمة نهجًا وظيفيًا وقابلًا للتركيب لمعالجة البيانات.

ما هي المكرِّرات غير المتزامنة والكائنات القابلة للتكرار غير المتزامن؟

قبل الخوض في مساعدي المكرِّرات غير المتزامنة، دعنا نفهم المفاهيم الأساسية: المكرِّرات غير المتزامنة (Async Iterators) والكائنات القابلة للتكرار غير المتزامن (Async Iterables).

الكائن القابل للتكرار غير المتزامن (Async Iterable) هو كائن يحدد طريقة للتكرار بشكل غير متزامن على قيمه. يقوم بذلك عن طريق تنفيذ الدالة @@asyncIterator، والتي تعيد مكرِّرًا غير متزامن (Async Iterator).

المكرِّر غير المتزامن (Async Iterator) هو كائن يوفر دالة next(). تعيد هذه الدالة وعدًا (promise) يتم حله إلى كائن له خاصيتان:

إليك مثال بسيط:


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 500)); // Simulate an asynchronous operation
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  for await (const value of asyncIterable) {
    console.log(value); // Output: 1, 2, 3, 4, 5 (with 500ms delay between each)
  }
})();

في هذا المثال، generateSequence هي دالة مولِّدة غير متزامنة تنتج تسلسلاً من الأرقام بشكل غير متزامن. تُستخدم حلقة for await...of لاستهلاك القيم من الكائن القابل للتكرار غير المتزامن.

تقديم مساعدي المكرِّرات غير المتزامنة

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

تشمل مساعدو المكرِّرات غير المتزامنة الأساسيون ما يلي:

دعنا نستكشف كل مساعد مع أمثلة.

map()

يقوم المساعد map() بتحويل كل عنصر من الكائن القابل للتكرار غير المتزامن باستخدام دالة معينة. يعيد كائنًا جديدًا قابلًا للتكرار غير متزامن مع القيم المحوَّلة.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const doubledIterable = asyncIterable.map(x => x * 2);

(async () => {
  for await (const value of doubledIterable) {
    console.log(value); // Output: 2, 4, 6, 8, 10 (with 100ms delay)
  }
})();

في هذا المثال، map(x => x * 2) تضاعف كل رقم في التسلسل.

filter()

يقوم المساعد filter() باختيار عناصر من الكائن القابل للتكرار غير المتزامن بناءً على شرط معين (دالة شرطية). يعيد كائنًا جديدًا قابلًا للتكرار غير متزامن يحتوي فقط على العناصر التي تفي بالشرط.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);

const evenNumbersIterable = asyncIterable.filter(x => x % 2 === 0);

(async () => {
  for await (const value of evenNumbersIterable) {
    console.log(value); // Output: 2, 4, 6, 8, 10 (with 100ms delay)
  }
})();

في هذا المثال، filter(x => x % 2 === 0) تختار الأرقام الزوجية فقط من التسلسل.

take()

يعيد المساعد take() أول N عنصر من الكائن القابل للتكرار غير المتزامن. يعيد كائنًا جديدًا قابلًا للتكرار غير متزامن يحتوي فقط على العدد المحدد من العناصر.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const firstThreeIterable = asyncIterable.take(3);

(async () => {
  for await (const value of firstThreeIterable) {
    console.log(value); // Output: 1, 2, 3 (with 100ms delay)
  }
})();

في هذا المثال، take(3) تختار الأرقام الثلاثة الأولى من التسلسل.

drop()

يتخطى المساعد drop() أول N عنصر من الكائن القابل للتكرار غير المتزامن ويعيد الباقي. يعيد كائنًا جديدًا قابلًا للتكرار غير متزامن يحتوي على العناصر المتبقية.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const afterFirstTwoIterable = asyncIterable.drop(2);

(async () => {
  for await (const value of afterFirstTwoIterable) {
    console.log(value); // Output: 3, 4, 5 (with 100ms delay)
  }
})();

في هذا المثال، drop(2) تتخطى الرقمين الأولين من التسلسل.

toArray()

يستهلك المساعد toArray() الكائن القابل للتكرار غير المتزامن بأكمله ويجمع كل العناصر في مصفوفة. يعيد وعدًا يتم حله إلى مصفوفة تحتوي على جميع العناصر.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const numbersArray = await asyncIterable.toArray();
  console.log(numbersArray); // Output: [1, 2, 3, 4, 5]
})();

في هذا المثال، toArray() تجمع كل الأرقام من التسلسل في مصفوفة.

forEach()

ينفذ المساعد forEach() دالة معينة مرة واحدة لكل عنصر في الكائن القابل للتكرار غير المتزامن. لا يعيد كائنًا جديدًا قابلًا للتكرار غير متزامن، بل ينفذ الدالة كتأثير جانبي. يمكن أن يكون هذا مفيدًا لتنفيذ عمليات مثل التسجيل أو تحديث واجهة المستخدم.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(3);

(async () => {
  await asyncIterable.forEach(value => {
    console.log("Value:", value);
  });
  console.log("forEach completed");
})();
// Output: Value: 1, Value: 2, Value: 3, forEach completed

some()

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


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const hasEvenNumber = await asyncIterable.some(x => x % 2 === 0);
  console.log("Has even number:", hasEvenNumber); // Output: Has even number: true
})();

every()

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


async function* generateSequence(end) {
  for (let i = 2; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(4);

(async () => {
  const areAllEven = await asyncIterable.every(x => x % 2 === 0);
  console.log("Are all even:", areAllEven); // Output: Are all even: true
})();

find()

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


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const firstEven = await asyncIterable.find(x => x % 2 === 0);
  console.log("First even number:", firstEven); // Output: First even number: 2
})();

reduce()

ينفذ المساعد reduce() دالة "reducer" مقدمة من المستخدم على كل عنصر من عناصر الكائن القابل للتكرار غير المتزامن، بالترتيب، ويمرر القيمة المرجعة من العملية على العنصر السابق. النتيجة النهائية لتشغيل الـ reducer على جميع العناصر هي قيمة واحدة. يعيد وعدًا يتم حله إلى القيمة المجمّعة النهائية.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const sum = await asyncIterable.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
  console.log("Sum:", sum); // Output: Sum: 15
})();

أمثلة عملية وحالات استخدام

مساعدو المكرِّرات غير المتزامنة لهم قيمة في سيناريوهات متنوعة. دعنا نستكشف بعض الأمثلة العملية:

1. معالجة البيانات من واجهة برمجة تطبيقات متدفقة (Streaming API)

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


async function* fetchDataFromAPI(url) {
  let response = await fetch(url);

  if (!response.body) {
    throw new Error("ReadableStream not supported in this environment");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        break;
      }
      const chunk = decoder.decode(value);
      // Assuming the API sends JSON objects separated by newlines
      const lines = chunk.split('\n');
      for (const line of lines) {
        if (line.trim() !== '') {
          yield JSON.parse(line);
        }
      }
    }
  } finally {
    reader.releaseLock();
  }
}

const apiURL = 'https://example.com/streaming-api'; // Replace with your API URL
const dataStream = fetchDataFromAPI(apiURL);

// Process the data stream
(async () => {
  for await (const data of dataStream.filter(item => item.type === 'metric').map(item => ({ timestamp: item.timestamp, value: item.value }))) {
    console.log('Processed Data:', data);
    // Update the dashboard with the processed data
  }
})();

في هذا المثال، تقوم fetchDataFromAPI بجلب البيانات من واجهة برمجة تطبيقات متدفقة، وتحليل كائنات JSON، وتوليدها ككائن قابل للتكرار غير متزامن. يقوم المساعد filter باختيار المقاييس فقط، ويقوم المساعد map بتحويل البيانات إلى التنسيق المطلوب قبل تحديث لوحة التحكم.

2. قراءة ومعالجة الملفات الكبيرة

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


async function* readLinesFromFile(filePath) {
  const file = await fsPromises.open(filePath, 'r');

  try {
    let buffer = Buffer.alloc(1024);
    let fileOffset = 0;
    let remainder = '';

    while (true) {
      const { bytesRead } = await file.read(buffer, 0, buffer.length, fileOffset);
      if (bytesRead === 0) {
        if (remainder) {
          yield remainder;
        }
        break;
      }

      fileOffset += bytesRead;
      const chunk = buffer.toString('utf8', 0, bytesRead);
      const lines = chunk.split('\n');

      lines[0] = remainder + lines[0];
      remainder = lines.pop() || '';

      for (const line of lines) {
        yield line;
      }
    }
  } finally {
    await file.close();
  }
}

const filePath = './customer_data.csv'; // Replace with your file path
const lines = readLinesFromFile(filePath);

// Process the lines
(async () => {
  for await (const customerData of lines.drop(1).map(line => line.split(',')).filter(data => data[2] === 'USA')) {
    console.log('Customer from USA:', customerData);
    // Process customer data from the USA
  }
})();

في هذا المثال، تقوم readLinesFromFile بقراءة الملف سطراً بسطر وتوليد كل سطر ككائن قابل للتكرار غير متزامن. يتخطى المساعد drop(1) صف الرأس، ويقوم المساعد map بتقسيم السطر إلى أعمدة، ويختار المساعد filter العملاء من الولايات المتحدة فقط.

3. التعامل مع الأحداث في الوقت الفعلي

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


async function* createWebSocketStream(url) {
  const ws = new WebSocket(url);

  yield new Promise((resolve, reject) => {
      ws.onopen = () => {
          resolve();
      };
      ws.onerror = (error) => {
          reject(error);
      };
  });

  try {
    while (ws.readyState === WebSocket.OPEN) {
      yield new Promise((resolve, reject) => {
        ws.onmessage = (event) => {
          resolve(JSON.parse(event.data));
        };
        ws.onerror = (error) => {
          reject(error);
        };
        ws.onclose = () => {
           resolve(null); // Resolve with null when connection closes
        }
      });

    }
  } finally {
    ws.close();
  }
}

const websocketURL = 'wss://example.com/events'; // Replace with your WebSocket URL
const eventStream = createWebSocketStream(websocketURL);

// Process the event stream
(async () => {
  for await (const event of eventStream.filter(event => event.type === 'user_login').map(event => ({ userId: event.userId, timestamp: event.timestamp }))) {
    console.log('User Login Event:', event);
    // Process user login event
  }
})();

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

فوائد استخدام مساعدي المكرِّرات غير المتزامنة

دعم المتصفحات وبيئات التشغيل

لا يزال مساعدو المكرِّرات غير المتزامنة ميزة جديدة نسبيًا في JavaScript. حتى أواخر عام 2024، هم في المرحلة الثالثة من عملية التقييس TC39، مما يعني أنه من المرجح أن يتم توحيدهم في المستقبل القريب. ومع ذلك، فهم غير مدعومين أصلاً في جميع المتصفحات وإصدارات Node.js.

دعم المتصفحات: تضيف المتصفحات الحديثة مثل Chrome وFirefox وSafari وEdge تدريجيًا دعمًا لمساعدي المكرِّرات غير المتزامنة. يمكنك التحقق من أحدث معلومات توافق المتصفحات على مواقع مثل Can I use... لمعرفة المتصفحات التي تدعم هذه الميزة.

دعم Node.js: توفر الإصدارات الحديثة من Node.js (v18 وما فوق) دعمًا تجريبيًا لمساعدي المكرِّرات غير المتزامنة. لاستخدامها، قد تحتاج إلى تشغيل Node.js مع العلامة --experimental-async-iterator.

Polyfills: إذا كنت بحاجة إلى استخدام مساعدي المكرِّرات غير المتزامنة في بيئات لا تدعمها أصلاً، يمكنك استخدام polyfill. الـ polyfill هو جزء من الكود يوفر الوظيفة المفقودة. تتوفر العديد من مكتبات polyfill لمساعدي المكرِّرات غير المتزامنة؛ أحد الخيارات الشائعة هو مكتبة core-js.

تنفيذ مكرِّرات غير متزامنة مخصصة

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

لإنشاء مكرِّر غير متزامن مخصص، تحتاج إلى تنفيذ الدالة @@asyncIterator على كائن. يجب أن تعيد هذه الدالة كائنًا به دالة next(). يجب أن تعيد دالة next() وعدًا يتم حله إلى كائن به خاصيتي value و done.

إليك مثال على مكرِّر غير متزامن مخصص يجلب البيانات من واجهة برمجة تطبيقات مقسمة إلى صفحات:


async function* fetchPaginatedData(baseURL) {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const url = `${baseURL}?page=${page}`;
    const response = await fetch(url);
    const data = await response.json();

    if (data.results.length === 0) {
      hasMore = false;
      break;
    }

    for (const item of data.results) {
      yield item;
    }

    page++;
  }
}

const apiBaseURL = 'https://api.example.com/data'; // Replace with your API URL
const paginatedData = fetchPaginatedData(apiBaseURL);

// Process the paginated data
(async () => {
  for await (const item of paginatedData) {
    console.log('Item:', item);
    // Process the item
  }
})();

في هذا المثال، تجلب fetchPaginatedData البيانات من واجهة برمجة تطبيقات مقسمة إلى صفحات، وتولد كل عنصر عند استرداده. يعالج المكرِّر غير المتزامن منطق التقسيم إلى صفحات، مما يسهل استهلاك البيانات بطريقة متدفقة.

التحديات والاعتبارات المحتملة

بينما يقدم مساعدو المكرِّرات غير المتزامنة العديد من الفوائد، من المهم أن تكون على دراية ببعض التحديات والاعتبارات المحتملة:

أفضل الممارسات لاستخدام مساعدي المكرِّرات غير المتزامنة

للحصول على أقصى استفادة من مساعدي المكرِّرات غير المتزامنة، ضع في اعتبارك أفضل الممارسات التالية:

تقنيات متقدمة

تكوين مساعدين مخصصين

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


async function* takeWhile(asyncIterable, predicate) {
  for await (const value of asyncIterable) {
    if (!predicate(value)) {
      break;
    }
    yield value;
  }
}

// Example Usage:
async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);
const firstFive = takeWhile(asyncIterable, x => x <= 5);

(async () => {
  for await (const value of firstFive) {
    console.log(value);
  }
})();

دمج عدة كائنات قابلة للتكرار غير المتزامن

يمكنك دمج عدة كائنات قابلة للتكرار غير متزامن في كائن واحد باستخدام تقنيات مثل zip أو merge. يتيح لك هذا معالجة البيانات من مصادر متعددة في وقت واحد.


async function* zip(asyncIterable1, asyncIterable2) {
    const iterator1 = asyncIterable1[Symbol.asyncIterator]();
    const iterator2 = asyncIterable2[Symbol.asyncIterator]();

    while (true) {
        const result1 = await iterator1.next();
        const result2 = await iterator2.next();

        if (result1.done || result2.done) {
            break;
        }

        yield [result1.value, result2.value];
    }
}

// Example Usage:
async function* generateSequence1(end) {
    for (let i = 1; i <= end; i++) {
        yield i;
    }
}

async function* generateSequence2(end) {
    for (let i = 10; i <= end + 9; i++) {
        yield i;
    }
}

const iterable1 = generateSequence1(5);
const iterable2 = generateSequence2(5);

(async () => {
    for await (const [value1, value2] of zip(iterable1, iterable2)) {
        console.log(value1, value2);
    }
})();

الخاتمة

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