Sobes.tech
Middle+

Dlaczego zwykłe generatory pozwalają pisać kod asynchroniczny?

sobes.tech AI

Odpowiedź od AI

Typowe generatory w JavaScript pozwalają pisać kod asynchroniczny dzięki ich zdolności do wstrzymywania i wznawiania wykonywania, a także do przekazywania wartości w obu kierunkach (za pomocą yield i next()).

Oto jak to działa w kontekście asynchroniczności:

  1. Wstrzymanie wykonania (yield): Gdy osiągnięty zostanie operator yield, generator jest wstrzymywany i zwraca wartość. W kodzie asynchronicznym, ta wartość może być obietnicą (promise) lub innym "działaniem", które musi zostać wykonane asynchronicznie.

  2. Oczekiwanie na wynik asynchroniczny: Zewnętrzny kod (np. "runner" lub opakowanie dla generatora) otrzymuje wartość przekazaną przez yield. Jeśli jest to promise, czeka na jego rozwiązanie lub odrzucenie.

  3. Wznowienie wykonania (next()): Po zakończeniu operacji asynchronicznej (rozwiązaniu promise), wynik asynchroniczny (wartość promise) jest przekazywany z powrotem do generatora przez metodę next(). Generator wznawia wykonanie od miejsca, w którym został wstrzymany.

  4. Przekazanie wyniku: Wartość przekazana do next() staje się wynikiem wyrażenia yield wewnątrz generatora. W ten sposób generator może "otrzymać" wynik operacji asynchronicznej i kontynuować z nim pracę, tak jakby kod był synchroniczny.

Ten wzorzec tworzy iluzję synchronicznego wykonania dla operacji asynchronicznych, czyniąc kod bardziej czytelnym i zarządzalnym w porównaniu z tradycyjnym użyciem callbacków lub skomplikowanych łańcuchów .then().

Przykładem tego są opakowania typu co (chociaż obecnie bardziej popularne są async/await, które są w zasadzie cukrem składniowym nad generatorami i promise'ami):

// Przykład użycia generatora do asynchroniczności
function* myAsyncGenerator() {
  try {
    const result1 = yield Promise.resolve(1); // Wstrzymanie, zwraca promise
    console.log('Pierwszy wynik:', result1); // Wznawianie z wartością

    const result2 = yield new Promise(resolve => setTimeout(() => resolve(2), 500)); // Kolejna operacja asynchroniczna
    console.log('Drugi wynik:', result2);

    return 'Gotowe'; // Zakończenie generatora
  } catch (error) {
    console.error('Wystąpił błąd:', error);
  }
}

// "Runner" prosty dla generatora (uproszczony)
function run(generatorFunc) {
  const generator = generatorFunc();

  function handleNext(newValue) {
    const result = generator.next(newValue);

    if (!result.done) {
      if (result.value instanceof Promise) {
        result.value.then(handleNext).catch(handleError);
      } else {
        handleNext(result.value);
      }
    } else {
      console.log('Generator zakończył z wynikiem:', result.value);
    }
  }

  function handleError(error) {
      generator.throw(error);
  }

  handleNext(); // Uruchom generator
}

// Uruchamiamy nasz asynchroniczny generator
run(myAsyncGenerator);

W tym przykładzie yield Promise.resolve(1) wstrzymuje generator. "Runner" (lub co) czeka, aż promise się rozwiąże, a następnie wznawia generator, przekazując wartość 1 przez next(1). Ta wartość 1 staje się wynikiem yield Promise.resolve(1) wewnątrz generatora, co pozwala na jej obsługę w następnej linii (console.log). Ten mechanizm pozwala na symulację synchronicznego przepływu wykonania dla operacji asynchronicznych.