webentwicklung-frage-antwort-db.com.de

Die beste Möglichkeit, ein Array zu durchlaufen, ohne die Benutzeroberfläche zu blockieren

Ich muss einige große Arrays durchlaufen und sie in Backbone-Sammlungen von einem API-Aufruf speichern. Was ist der beste Weg, dies zu tun, ohne dass die Schleife dazu führt, dass die Schnittstelle nicht mehr reagiert?

Die Rückgabe der Ajax-Anforderung blockiert auch, da die zurückgegebenen Daten so groß sind. Ich denke, ich könnte es aufteilen und setTimeout verwenden, um es asynchron in kleineren Chunks auszuführen, aber es gibt einen einfacheren Weg, dies zu tun.

Ich dachte, ein Web-Worker wäre gut, aber er muss einige Datenstrukturen ändern, die im UI-Thread gespeichert sind. Ich habe damit versucht, den Ajax-Aufruf auszuführen, aber wenn er die Daten an den UI-Thread zurückgibt, gibt es immer noch eine Zeit, zu der die Schnittstelle nicht reagiert.

Danke im Voraus

49
georgephillips

Sie haben die Wahl zwischen mit oder ohne Webworker:

Ohne Webworker

Für Code, der mit dem DOM oder mit vielen anderen Status in Ihrer App interagieren muss, können Sie keinen WebWorker verwenden. Die übliche Lösung besteht darin, Ihre Arbeit in Blöcke aufzuteilen, die jeden Teil der Arbeit mit einem Timer erledigen. Die Unterbrechung zwischen Blöcken mit dem Timer ermöglicht es der Browser-Engine, andere Ereignisse zu verarbeiten, die gerade stattfinden, und ermöglicht nicht nur die Verarbeitung von Benutzereingaben, sondern auch das Zeichnen des Bildschirms.

Normalerweise können Sie es sich leisten, mit jedem Timer mehr als einen zu verarbeiten, was effizienter und schneller ist als nur ein Timer pro Timer. Dieser Code gibt dem UI-Thread die Möglichkeit, alle anstehenden UI-Ereignisse zwischen jedem Block zu verarbeiten, wodurch die UI aktiv bleibt.

function processLargeArray(array) {
    // set this to whatever number of items you can process at once
    var chunk = 100;
    var index = 0;
    function doChunk() {
        var cnt = chunk;
        while (cnt-- && index < array.length) {
            // process array[index] here
            ++index;
        }
        if (index < array.length) {
            // set Timeout for async iteration
            setTimeout(doChunk, 1);
        }
    }    
    doChunk();    
}

processLargeArray(veryLargeArray);

Hier ist ein funktionierendes Beispiel für das Konzept - nicht die gleiche Funktion, sondern ein anderer Prozess mit langer Laufzeit, der dieselbe setTimeout()-Idee verwendet, um ein Wahrscheinlichkeitsszenario mit vielen Iterationen zu testen: http://jsfiddle.net/jfriend00/9hCVq/


Sie können das Obige in eine allgemeinere Version umwandeln, die eine Callback-Funktion wie .forEach() aufruft:

// last two args are optional
function processLargeArrayAsync(array, fn, chunk, context) {
    context = context || window;
    chunk = chunk || 100;
    var index = 0;
    function doChunk() {
        var cnt = chunk;
        while (cnt-- && index < array.length) {
            // callback called with args (value, index, array)
            fn.call(context, array[index], index, array);
            ++index;
        }
        if (index < array.length) {
            // set Timeout for async iteration
            setTimeout(doChunk, 1);
        }
    }    
    doChunk();    
}

processLargeArrayAsync(veryLargeArray, myCallback, 100);

Anstatt zu erraten, wie viele auf einmal abgelegt werden sollen, ist es auch möglich, die abgelaufene Zeit als Leitfaden für jeden Abschnitt zu verwenden und es so viele wie möglich in einem bestimmten Zeitintervall verarbeiten zu lassen. Dies garantiert in gewissem Maße automatisch die Reaktionsfähigkeit des Browsers, unabhängig von der CPU-intensiven Iteration. Anstatt eine Blockgröße zu übergeben, können Sie also einen Millisekundenwert übergeben (oder einfach eine intelligente Standardeinstellung verwenden):

// last two args are optional
function processLargeArrayAsync(array, fn, maxTimePerChunk, context) {
    context = context || window;
    maxTimePerChunk = maxTimePerChunk || 200;
    var index = 0;

    function now() {
        return new Date().getTime();
    }

    function doChunk() {
        var startTime = now();
        while (index < array.length && (now() - startTime) <= maxTimePerChunk) {
            // callback called with args (value, index, array)
            fn.call(context, array[index], index, array);
            ++index;
        }
        if (index < array.length) {
            // set Timeout for async iteration
            setTimeout(doChunk, 1);
        }
    }    
    doChunk();    
}

processLargeArrayAsync(veryLargeArray, myCallback);

Mit Webworkern

Wenn der Code in Ihrer Schleife nicht auf das DOM zugreifen muss, ist es möglich, den gesamten zeitaufwändigen Code in einen Webworker zu geben. Der webWorker wird unabhängig vom Hauptbrowser Javascript ausgeführt und kann anschließend alle Ergebnisse mit einer postMessage zurückmelden.

Ein WebWorker erfordert, dass der gesamte Code, der in WebWorker ausgeführt wird, in einer separaten Skriptdatei getrennt wird. Er kann jedoch vollständig ausgeführt werden, ohne dass die Verarbeitung anderer Ereignisse im Browser blockiert werden muss und ohne dass das "nicht reagierende Skript" dazu aufgefordert wird kann auftauchen, wenn ein langer Prozess im Haupt-Thread ausgeführt wird.

75
jfriend00

Hier ist eine Demo dieser "async" -Schleife. es "verzögert" die Iteration um 1 ms und gibt der Benutzeroberfläche innerhalb dieser Verzögerung die Möglichkeit, etwas zu tun.

function asyncLoop(arr, callback) {
    (function loop(i) {

        //do stuff here

        if (i < arr.Length) {                      //the condition
            setTimeout(function() {loop(++i)}, 1); //rerun when condition is true
        } else { 
            callback();                            //callback when the loop ends
        }
    }(0));                                         //start with 0
}

asyncLoop(yourArray, function() {
    //do after loop  
})​;

//anything down here runs while the loop runs

Es gibt Alternativen wie web workers und das derzeit vorgeschlagene setImmediate , wobei afaik auf IE mit einem Präfix steht.

5
Joseph

Aufbauend auf @ jfriend00 ist hier eine Prototypversion:

if (Array.prototype.forEachAsync == null) {
    Array.prototype.forEachAsync = function forEachAsync(fn, thisArg, maxTimePerChunk, callback) {
        let that = this;
        let args = Array.from(arguments);

        let lastArg = args.pop();

        if (lastArg instanceof Function) {
            callback = lastArg;
            lastArg = args.pop();
        } else {
            callback = function() {};
        }
        if (Number(lastArg) === lastArg) {
            maxTimePerChunk = lastArg;
            lastArg = args.pop();
        } else {
            maxTimePerChunk = 200;
        }
        if (args.length === 1) {
            thisArg = lastArg;
        } else {
            thisArg = that
        }

        let index = 0;

        function now() {
            return new Date().getTime();
        }

        function doChunk() {
            let startTime = now();
            while (index < that.length && (now() - startTime) <= maxTimePerChunk) {
                // callback called with args (value, index, array)
                fn.call(thisArg, that[index], index, that);
                ++index;
            }
            if (index < that.length) {
                // set Timeout for async iteration
                setTimeout(doChunk, 1);
            } else {
                callback();
            }
        }

        doChunk();
    }
}
0
cjbarth