Polski

Odkryj JavaScript Async Iterator Helpers, aby zrewolucjonizować przetwarzanie strumieni. Naucz się efektywnie obsługiwać asynchroniczne strumienie danych za pomocą map, filter, take, drop i innych.

JavaScript Async Iterator Helpers: Wydajne przetwarzanie strumieniowe dla nowoczesnych aplikacji

W nowoczesnym programowaniu w JavaScript, obsługa asynchronicznych strumieni danych jest częstym wymogiem. Niezależnie od tego, czy pobierasz dane z API, przetwarzasz duże pliki, czy obsługujesz zdarzenia w czasie rzeczywistym, efektywne zarządzanie danymi asynchronicznymi jest kluczowe. JavaScript Async Iterator Helpers zapewniają potężny i elegancki sposób na przetwarzanie tych strumieni, oferując funkcyjne i kompozycyjne podejście do manipulacji danymi.

Czym są Async Iterators i Async Iterables?

Zanim zagłębimy się w Async Iterator Helpers, zrozummy podstawowe pojęcia: Async Iterators i Async Iterables.

Async Iterable to obiekt, który definiuje sposób asynchronicznego iterowania po swoich wartościach. Robi to poprzez implementację metody @@asyncIterator, która zwraca Async Iterator.

Async Iterator to obiekt, który dostarcza metodę next(). Metoda ta zwraca obietnicę (promise), która rozwiązuje się do obiektu z dwiema właściwościami:

Oto prosty przykład:


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

const asyncIterable = generateSequence(5);

(async () => {
  for await (const value of asyncIterable) {
    console.log(value); // Wynik: 1, 2, 3, 4, 5 (z 500ms opóźnieniem między każdym)
  }
})();

W tym przykładzie generateSequence to asynchroniczna funkcja generatora, która produkuje sekwencję liczb asynchronicznie. Pętla for await...of jest używana do konsumowania wartości z asynchronicznego obiektu iterowalnego.

Wprowadzenie do Async Iterator Helpers

Async Iterator Helpers rozszerzają funkcjonalność Async Iterators, dostarczając zestaw metod do transformacji, filtrowania i manipulowania asynchronicznymi strumieniami danych. Umożliwiają one funkcyjny i kompozycyjny styl programowania, ułatwiając budowanie złożonych potoków przetwarzania danych.

Główne metody Async Iterator Helpers to:

Przeanalizujmy każdą z tych metod na przykładach.

map()

Metoda map() transformuje każdy element asynchronicznego obiektu iterowalnego przy użyciu podanej funkcji. Zwraca nowy asynchroniczny obiekt iterowalny z przekształconymi wartościami.


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); // Wynik: 2, 4, 6, 8, 10 (z 100ms opóźnieniem)
  }
})();

W tym przykładzie map(x => x * 2) podwaja każdą liczbę w sekwencji.

filter()

Metoda filter() wybiera elementy z asynchronicznego obiektu iterowalnego na podstawie podanego warunku (funkcji predykatywnej). Zwraca nowy asynchroniczny obiekt iterowalny zawierający tylko te elementy, które spełniają warunek.


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); // Wynik: 2, 4, 6, 8, 10 (z 100ms opóźnieniem)
  }
})();

W tym przykładzie filter(x => x % 2 === 0) wybiera tylko parzyste liczby z sekwencji.

take()

Metoda take() zwraca N pierwszych elementów z asynchronicznego obiektu iterowalnego. Zwraca nowy asynchroniczny obiekt iterowalny zawierający tylko określoną liczbę elementów.


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); // Wynik: 1, 2, 3 (z 100ms opóźnieniem)
  }
})();

W tym przykładzie take(3) wybiera trzy pierwsze liczby z sekwencji.

drop()

Metoda drop() pomija N pierwszych elementów z asynchronicznego obiektu iterowalnego i zwraca resztę. Zwraca nowy asynchroniczny obiekt iterowalny zawierający pozostałe elementy.


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); // Wynik: 3, 4, 5 (z 100ms opóźnieniem)
  }
})();

W tym przykładzie drop(2) pomija dwie pierwsze liczby z sekwencji.

toArray()

Metoda toArray() przetwarza cały asynchroniczny obiekt iterowalny i zbiera wszystkie elementy do tablicy. Zwraca obietnicę, która rozwiązuje się do tablicy zawierającej wszystkie elementy.


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); // Wynik: [1, 2, 3, 4, 5]
})();

W tym przykładzie toArray() zbiera wszystkie liczby z sekwencji do tablicy.

forEach()

Metoda forEach() wykonuje podaną funkcję raz dla każdego elementu w asynchronicznym obiekcie iterowalnym. *Nie* zwraca nowego asynchronicznego obiektu iterowalnego, wykonuje funkcję dla jej efektów ubocznych. Może to być przydatne do wykonywania operacji takich jak logowanie lub aktualizacja interfejsu użytkownika.


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");
})();
// Wynik: Value: 1, Value: 2, Value: 3, forEach completed

some()

Metoda some() sprawdza, czy co najmniej jeden element w asynchronicznym obiekcie iterowalnym przechodzi test zaimplementowany w podanej funkcji. Zwraca obietnicę, która rozwiązuje się do wartości logicznej (true, jeśli co najmniej jeden element spełnia warunek, w przeciwnym razie 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); // Wynik: Has even number: true
})();

every()

Metoda every() sprawdza, czy wszystkie elementy w asynchronicznym obiekcie iterowalnym przechodzą test zaimplementowany w podanej funkcji. Zwraca obietnicę, która rozwiązuje się do wartości logicznej (true, jeśli wszystkie elementy spełniają warunek, w przeciwnym razie 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); // Wynik: Are all even: true
})();

find()

Metoda find() zwraca pierwszy element w asynchronicznym obiekcie iterowalnym, który spełnia podaną funkcję testującą. Jeśli żadna wartość nie spełnia funkcji testującej, zwracane jest undefined. Zwraca obietnicę, która rozwiązuje się do znalezionego elementu lub 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); // Wynik: First even number: 2
})();

reduce()

Metoda reduce() wykonuje dostarczoną przez użytkownika funkcję zwrotną ("reducer") na każdym elemencie asynchronicznego obiektu iterowalnego, w kolejności, przekazując wartość zwrotną z obliczenia na poprzednim elemencie. Ostatecznym wynikiem uruchomienia reducera na wszystkich elementach jest pojedyncza wartość. Zwraca obietnicę, która rozwiązuje się do ostatecznej skumulowanej wartości.


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); // Wynik: Sum: 15
})();

Praktyczne przykłady i przypadki użycia

Async Iterator Helpers są cenne w różnych scenariuszach. Przyjrzyjmy się kilku praktycznym przykładom:

1. Przetwarzanie danych z API strumieniowego

Wyobraź sobie, że budujesz pulpit nawigacyjny do wizualizacji danych w czasie rzeczywistym, który otrzymuje dane z API strumieniowego. API wysyła aktualizacje w sposób ciągły, a Ty musisz przetwarzać te aktualizacje, aby wyświetlić najnowsze informacje.


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

  if (!response.body) {
    throw new Error("ReadableStream nie jest obsługiwany w tym środowisku");
  }

  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);
      // Zakładając, że API wysyła obiekty JSON oddzielone znakami nowej linii
      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'; // Zastąp swoim adresem URL API
const dataStream = fetchDataFromAPI(apiURL);

// Przetwarzaj strumień danych
(async () => {
  for await (const data of dataStream.filter(item => item.type === 'metric').map(item => ({ timestamp: item.timestamp, value: item.value }))) {
    console.log('Przetworzone dane:', data);
    // Zaktualizuj pulpit nawigacyjny przetworzonymi danymi
  }
})();

W tym przykładzie fetchDataFromAPI pobiera dane z API strumieniowego, parsuje obiekty JSON i udostępnia je jako asynchroniczny obiekt iterowalny. Metoda filter wybiera tylko metryki, a metoda map przekształca dane do pożądanego formatu przed aktualizacją pulpitu nawigacyjnego.

2. Odczytywanie i przetwarzanie dużych plików

Załóżmy, że musisz przetworzyć duży plik CSV zawierający dane klientów. Zamiast ładować cały plik do pamięci, możesz użyć Async Iterator Helpers, aby przetwarzać go kawałek po kawałku.


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'; // Zastąp ścieżką do swojego pliku
const lines = readLinesFromFile(filePath);

// Przetwarzaj linie
(async () => {
  for await (const customerData of lines.drop(1).map(line => line.split(',')).filter(data => data[2] === 'USA')) {
    console.log('Klient z USA:', customerData);
    // Przetwarzaj dane klientów z USA
  }
})();

W tym przykładzie readLinesFromFile odczytuje plik linia po linii i udostępnia każdą linię jako asynchroniczny obiekt iterowalny. Metoda drop(1) pomija wiersz nagłówka, metoda map dzieli linię na kolumny, a metoda filter wybiera tylko klientów z USA.

3. Obsługa zdarzeń w czasie rzeczywistym

Async Iterator Helpers mogą być również używane do obsługi zdarzeń w czasie rzeczywistym ze źródeł takich jak WebSockets. Możesz utworzyć asynchroniczny obiekt iterowalny, który emituje zdarzenia w miarę ich nadejścia, a następnie użyć metod pomocniczych do przetwarzania tych zdarzeń.


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); // Rozstrzygnij z wartością null, gdy połączenie zostanie zamknięte
        }
      });

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

const websocketURL = 'wss://example.com/events'; // Zastąp swoim adresem URL WebSocket
const eventStream = createWebSocketStream(websocketURL);

// Przetwarzaj strumień zdarzeń
(async () => {
  for await (const event of eventStream.filter(event => event.type === 'user_login').map(event => ({ userId: event.userId, timestamp: event.timestamp }))) {
    console.log('Zdarzenie logowania użytkownika:', event);
    // Przetwarzaj zdarzenie logowania użytkownika
  }
})();

W tym przykładzie createWebSocketStream tworzy asynchroniczny obiekt iterowalny, który emituje zdarzenia otrzymane z WebSocket. Metoda filter wybiera tylko zdarzenia logowania użytkownika, a metoda map przekształca dane do pożądanego formatu.

Korzyści z używania Async Iterator Helpers

Wsparcie w przeglądarkach i środowiskach uruchomieniowych

Async Iterator Helpers to wciąż stosunkowo nowa funkcja w JavaScript. Pod koniec 2024 roku znajdują się one w 3. etapie procesu standaryzacji TC39, co oznacza, że prawdopodobnie zostaną wkrótce znormalizowane. Jednak nie są one jeszcze natywnie obsługiwane we wszystkich przeglądarkach i wersjach Node.js.

Wsparcie w przeglądarkach: Nowoczesne przeglądarki, takie jak Chrome, Firefox, Safari i Edge, stopniowo dodają wsparcie dla Async Iterator Helpers. Możesz sprawdzić najnowsze informacje o kompatybilności przeglądarek na stronach takich jak Can I use..., aby zobaczyć, które przeglądarki obsługują tę funkcję.

Wsparcie w Node.js: Najnowsze wersje Node.js (v18 i nowsze) zapewniają eksperymentalne wsparcie dla Async Iterator Helpers. Aby z nich skorzystać, może być konieczne uruchomienie Node.js z flagą --experimental-async-iterator.

Polyfille: Jeśli musisz używać Async Iterator Helpers w środowiskach, które natywnie ich nie obsługują, możesz użyć polyfilla. Polyfill to fragment kodu, który zapewnia brakującą funkcjonalność. Dostępnych jest kilka bibliotek polyfill dla Async Iterator Helpers; popularną opcją jest biblioteka core-js.

Implementacja niestandardowych asynchronicznych iteratorów

Chociaż Async Iterator Helpers zapewniają wygodny sposób na przetwarzanie istniejących asynchronicznych obiektów iterowalnych, czasami może być konieczne stworzenie własnych niestandardowych iteratorów asynchronicznych. Pozwala to na obsługę danych z różnych źródeł, takich jak bazy danych, API czy systemy plików, w sposób strumieniowy.

Aby utworzyć niestandardowy iterator asynchroniczny, musisz zaimplementować metodę @@asyncIterator na obiekcie. Metoda ta powinna zwracać obiekt z metodą next(). Metoda next() powinna zwracać obietnicę, która rozwiązuje się do obiektu z właściwościami value i done.

Oto przykład niestandardowego iteratora asynchronicznego, który pobiera dane z paginowanego API:


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'; // Zastąp swoim adresem URL API
const paginatedData = fetchPaginatedData(apiBaseURL);

// Przetwarzaj dane paginowane
(async () => {
  for await (const item of paginatedData) {
    console.log('Element:', item);
    // Przetwarzaj element
  }
})();

W tym przykładzie fetchPaginatedData pobiera dane z paginowanego API, udostępniając każdy element w miarę jego pobierania. Iterator asynchroniczny obsługuje logikę paginacji, ułatwiając konsumowanie danych w sposób strumieniowy.

Potencjalne wyzwania i uwagi

Chociaż Async Iterator Helpers oferują liczne korzyści, ważne jest, aby być świadomym pewnych potencjalnych wyzwań i uwag:

Dobre praktyki korzystania z Async Iterator Helpers

Aby w pełni wykorzystać Async Iterator Helpers, rozważ następujące dobre praktyki:

Zaawansowane techniki

Komponowanie niestandardowych metod pomocniczych

Możesz tworzyć własne niestandardowe metody pomocnicze iteratora asynchronicznego, komponując istniejące metody lub budując nowe od podstaw. Pozwala to dostosować funkcjonalność do konkretnych potrzeb i tworzyć komponenty wielokrotnego użytku.


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

// Przykład użycia:
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);
  }
})();

Łączenie wielu asynchronicznych obiektów iterowalnych

Możesz łączyć wiele asynchronicznych obiektów iterowalnych w jeden, używając technik takich jak zip lub merge. Pozwala to na jednoczesne przetwarzanie danych z wielu źródeł.


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];
    }
}

// Przykład użycia:
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);
    }
})();

Podsumowanie

JavaScript Async Iterator Helpers zapewniają potężny i elegancki sposób przetwarzania asynchronicznych strumieni danych. Oferują one funkcyjne i kompozycyjne podejście do manipulacji danymi, ułatwiając budowanie złożonych potoków przetwarzania danych. Rozumiejąc podstawowe koncepcje Async Iterators i Async Iterables oraz opanowując różne metody pomocnicze, możesz znacznie poprawić wydajność i łatwość utrzymania swojego asynchronicznego kodu JavaScript. W miarę jak wsparcie w przeglądarkach i środowiskach uruchomieniowych będzie rosło, Async Iterator Helpers staną się niezbędnym narzędziem dla nowoczesnych programistów JavaScript.