Der beste Weg, um über ein Array zu iterieren, ohne die Benutzeroberfläche zu blockieren

70

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

Die Rückgabe der Ajax-Anforderung wird ebenfalls blockiert, da die zurückgegebenen Daten so groß sind. Ich denke, ich könnte es aufteilen und setTimeout verwenden, um es in kleineren Blöcken asynchron laufen zu lassen, 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 versucht, damit den Ajax-Aufruf auszuführen, aber wenn die Daten an den UI-Thread zurückgegeben werden, reagiert die Schnittstelle immer noch nicht mehr.

Danke im Voraus

Georgephillips
quelle
Am Ende habe ich das von @ jfriend00 vorgeschlagene Beispiel mit ein paar Änderungen verwendet, um unterschiedliche Schlüsselwerte und eine kleinere
Blockgröße

Antworten:

107

Sie haben die Wahl zwischen mit oder ohne webWorkers:

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, um jeden Teil der Arbeit an einem Timer auszuführen. 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, mehr als einen auf jedem Timer zu verarbeiten, was sowohl effizienter als auch schneller ist als nur einen pro Timer. Dieser Code gibt dem UI-Thread die Möglichkeit, ausstehende 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 dieselbe 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 oben Genannte in eine allgemeinere Version umwandeln, die eine Rückruffunktion wie .forEach()folgt 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 Chunks gleichzeitig benötigt werden, ist es auch möglich, die verstrichene Zeit als Leitfaden für jeden Chunk zu verwenden und so viele wie möglich in einem bestimmten Zeitintervall verarbeiten zu lassen. Dies garantiert automatisch die Reaktionsfähigkeit des Browsers, unabhängig davon, wie CPU-intensiv die Iteration ist. Anstatt eine Blockgröße zu übergeben, können Sie einen Millisekundenwert übergeben (oder einfach einen intelligenten Standard 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, können Sie den gesamten zeitaufwändigen Code in einen WebWorker einfügen. Der webWorker wird unabhängig vom Hauptbrowser Javascript ausgeführt und kann dann, wenn er fertig ist, alle Ergebnisse mit einer postMessage zurückmelden.

Ein WebWorker muss den gesamten Code, der im WebWorker ausgeführt wird, in eine separate Skriptdatei aufteilen. Er kann jedoch vollständig ausgeführt werden, ohne dass die Verarbeitung anderer Ereignisse im Browser blockiert werden muss und ohne dass die Eingabeaufforderung "Nicht reagierendes Skript" erforderlich ist Dies kann auftreten, wenn ein lang laufender Prozess im Hauptthread ausgeführt wird und die Ereignisverarbeitung in der Benutzeroberfläche nicht blockiert wird.

jfriend00
quelle
Es wurde eine allgemeinere Version hinzugefügt, die über einen .forEach()Stilrückruf funktioniert, sodass dieselbe Dienstprogrammfunktion für viele Zwecke verwendet werden kann.
jfriend00
1
Es wurde eine weitere Allzweckversion hinzugefügt, die nach Zeit statt nach Menge unterteilt, sodass die eigene Blockgröße basierend auf der Dauer der Iteration angepasst wird (der Browser reagiert).
jfriend00
Wie würde dies auf die for..inObjektaufzählung zutreffen ? Erstellen Sie ein Array, dann die oben genannten? (Oder besser eine neue Frage stellen?)
serv-inc
2
@user - Ja, Sie müssten zuerst das Array erstellen (Sie könnten es Object.keys()zum Erstellen des Arrays verwenden), da Sie auf diese Weise nicht direkt mit iterieren können for/in.
jfriend00
1
Ich denke, window.requestAnimationFrame()anstelle von a setTimeout()ist viel besser. Auf diese Weise sind Sie absolut sicher, dass Ihr Code nicht blockiert, da der Browser Ihnen sagt, dass es cool ist, etwas zu verarbeiten.
Dodov
5

Hier ist eine Demo dieser "asynchronen" Schleife. Es "verzögert" die Iteration um 1 ms und innerhalb dieser Verzögerung gibt es der Benutzeroberfläche 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-Worker und das derzeit vorgeschlagene setImmediate, das afaik im IE mit einem Präfix ist.

Joseph
quelle
Nicht so gut wie die vorherige Antwort, da nach jedem Element setTimeout aufgerufen wird .
Andrey M.
Die Verzögerung von 1 ms ist nicht der Grund, warum die Benutzeroberfläche aktualisiert wird. Die setTimeoutFunktion stellt die Funktion in die Rückrufwarteschlange, die abgeholt wird, nachdem die Benutzeroberfläche die Möglichkeit hatte, ihre Aufgabe zu erledigen. Sie können genauso einfach eine Verzögerung von 0 Sekunden eingeben.
chris
0

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();
    }
}
cjbarth
quelle
anstatt alles hinein zu ifif (...) return; ...
stecken,
0

Vielen Dank dafür.

Ich habe den Code aktualisiert, um einige Funktionen hinzuzufügen.

Mit dem folgenden Code können Sie entweder die Funktion für Arrays (zum Iterieren von Arrays) oder die Funktion für Karten (zum Iterieren von Karten) verwenden.

Außerdem gibt es jetzt einen Parameter für eine Funktion, die aufgerufen wird, wenn ein Block abgeschlossen ist (hilft, wenn Sie eine Lademeldung aktualisieren müssen), sowie einen Parameter für eine Funktion, die am Ende der Verarbeitung der Schleife aufgerufen wird (erforderlich, um die nächste auszuführen Schritt nach Abschluss der asynchronen Operationen)

//Iterate Array Asynchronously
//fn = the function to call while iterating over the array (for loop function call)
//chunkEndFn (optional, use undefined if not using) = the function to call when the chunk ends, used to update a loading message
//endFn (optional, use undefined if not using) = called at the end of the async execution
//last two args are optional
function iterateArrayAsync(array, fn, chunkEndFn, endFn, 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((now() - startTime) > maxTimePerChunk && chunkEndFn !== undefined){
            //callback called with args (index, length)
            chunkEndFn.call(context,index,array.length);
        }
        if (index < array.length) {
            // set Timeout for async iteration
            setTimeout(doChunk, 1);
        }
        else if(endFn !== undefined){
            endFn.call(context);
        }
    }    
    doChunk();    
}

//Usage
iterateArrayAsync(ourArray,function(value, index, array){
    //runs each iteration of the loop
},
function(index,length){
    //runs after every chunk completes, this is optional, use undefined if not using this
},
function(){
    //runs after completing the loop, this is optional, use undefined if not using this

});

//Iterate Map Asynchronously
//fn = the function to call while iterating over the map (for loop function call)
//chunkEndFn (optional, use undefined if not using) = the function to call when the chunk ends, used to update a loading message
//endFn (optional, use undefined if not using) = called at the end of the async execution
//last two args are optional
function iterateMapAsync(map, fn, chunkEndFn, endFn, maxTimePerChunk, context) {
    var array = Array.from(map.keys());
    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, key, map)
            fn.call(context,map.get(array[index]), array[index], map);
            ++index;
        }
        if((now() - startTime) > maxTimePerChunk && chunkEndFn !== undefined){
            //callback called with args (index, length)
            chunkEndFn.call(context,index,array.length);
        }
        if (index < array.length) {
            // set Timeout for async iteration
            setTimeout(doChunk, 1);
        }
        else if(endFn !== undefined){
            endFn.call(context);
        }
    }    
    doChunk();
}

//Usage
iterateMapAsync(ourMap,function(value, key, map){
    //runs each iteration of the loop
},
function(index,length){
    //runs after every chunk completes, this is optional, use undefined if not using this
},
function(){
    //runs after completing the loop, this is optional, use undefined if not using this

});
wootowl
quelle
Das Aufrufen dieser Funktion asyncist wohl irreführend. Die Operationen sind immer noch auf demselben Thread synchron.
canbax