Antwort an „Rolf B“ verfassen

Hallo Jürgen und Michael,

es gibt diverse Ansätze - aber welcher passt, dafür muss man den Anwendungsfall kennen. Darum habe ich danach gefragt.

requestAnimationFrame hängt an der Framerate und am Layout-Zyklus. Ob das bei einer zeitintensiven Hintergrundberechnung ideal ist, weiß ich nicht. Ich würde deshalb lieber setTimeout(func, 0) nehmen. Ich habe auch mal queueMicrotask probiert, aber wenn eine Funktion sich selbst als Microtask queued, blockiert das UI.

Grundsätzlich ist ein Hintergrundprozess, der mit dem DOM rummacht, gefährlich. Das DOM kann sich durch Benutzerinteraktionen verändern, und der Benutzer kann durch DOM-Änderungen, die der Hintergrundprozess macht, behindert werden. Das DOM ist aus gutem Grund single-threaded. Der Use-Case muss daher sehr speziell sein und der Rest der Seite darauf abgestimmt sein, dass da ein Worker tobt. In Jürgens Beispiel ist das so - der Worker ermittelt, was zu zeichnen ist und seine Effekte sind auf den Canvas begrenzt.

Wenn ein Worker - weshalb auch immer - nicht praktikabel ist, muss man sich den UI-Thread mit dem Benutzer teilen. Und man muss gut überlegen, ob man nicht den Stand des DOM, mit dem man anfängt, als Schnappschuss speichern sollte.

In Windows 2 gab es die yield-Funktion, mit der ein Prozess die CPU freiwillig abgeben konnte. Das gibt's heute nicht mehr, aber es gibt ein yield-Statement in JavaScript. Das versteckt sich in Generatorfunktionen.

Die dienen zwar eigentlich zur Erzeugung einer Wertefolge, aber konzeptionell bieten sie mit dem yield-Befehl zwei wichtige Möglichkeiten:

  • Suspendieren des Generatorablaufs, ohne den Kontext zu verlieren. Insbesondere kann ein yield tief in irgendwelchen Strukturblöcken stecken. Zwar nicht in Funktionen, die der Generator aufruft, aber die kann man zur Not als Untergenerator schreiben.
  • Hereingeben von Steuerdaten aus der Iteratorschleife

Hier als Beispiel eine sehr simple Verarbeitungsschleife, die als Generator verpackt ist. Sie durchläuft ein Array und tut pro Element irgendwas. Die Variablen foo und bar speichern übergreifende Daten für die Varbeitung.

Da der Rückgabewert eines Generators ein Iterator ist, kann man keine eigenen Werte zurückgeben. Ein result-Parameter, in dem Rückgabewerte platziert werden können, umgeht das.

Entscheidender Punkt ist die yield-Anweisung, an der der Generator unterbricht und dorthin zurückkehrt, wo next() aufgerufen wurde. Im Generator selbst muss lediglich "oft genug" yield aufgerufen werden. Die Zeitsteuerung wird vom Iterierer übernommen.

function* runComplexThing(data, result) {
   let foo = 0, bar = 0;
   result.value = undefined;

   for (let i=0; i<data.length; i++) {
     // data[i] verarbeiten;
 
     if (yield i) break;;
   }
}

Ein Generator muss mit einer Iteration durchlaufen werden. Dazu kann man for...of verwenden, aber in einer for...of Schleife lässt sich der JavaScript-Ablauf nicht unterbrechen. Die Alternative zu for...of ist das manuelle Durchlaufen mit der next()-Methode:

const result = {};
const dataWringer = runComplexThing(data, result);
runWringStep(dataWringer, (aborted) => handleResult(result, aborted));

function runWringStep(generator, onComplete) {
  // Maximale Step-Zeit: 100ms (beispielsweise)
  const stepStart = Date.now();
  while (Date.now() - stepStart < 100) { 

    // Optional: Abbruchbedingung, z.B. Timeout oder Cancel-Button
    const abort = /* true um abzubrechen, false zum weitermachen */

    // Wert von abort ist Ergebnis von yield
    const genValue = generator.next(abort);  
    // genValue enthält den Iteratorstatus
    if (genValue.done) {
       onComplete(abort);
       return;
    }
  }
  setTimeout(runWringStep, 0, generator, onComplete);
}

Dieses Snippet durchläuft den Generator, bis die Generatorfunktion endet (done ist dann false). Wenn nach einer Anzahl von next()-Aufrufen zu viel Zeit vergangen ist, wird setTimeout verwendet, um zunächst das UI zu bedienen und den Generator im nächsten Durchlauf der JavaScript-Eventloop weiter zu verarbeiten. Die Zeit von 100ms muss man an den Anwendungsfall anpassen, 100 ist aber schon die Obergrenze für halbwegs flüssiges UI.

Wenn man einen Abbruchmöglichkeit vorsehen will, kann man das auf unterschiedliche Art tun. Im Beispiel übergebe ich ein bool an next(), was zum Rückgabewert von yield wird. Statt dessen kann man auch die return()- oder throw()-Methode des Generators verwenden.

Ich denke, daraus könnte man einen Wiki-Artikel machen: Vordergrund-Worker mit Generatoren. Dort würde ich das Ergebnis noch in ein Promise verpacken und einen AbortController unterstützen…

Rolf

--
sumpsi - posui - obstruxi
freiwillig, öffentlich sichtbar
freiwillig, öffentlich sichtbar
freiwillig, öffentlich sichtbar

Ihre Identität in einem Cookie zu speichern erlaubt es Ihnen, Ihre Beiträge zu editieren. Außerdem müssen Sie dann bei neuen Beiträgen nicht mehr die Felder Name, E-Mail und Homepage ausfüllen.

abbrechen