web-dev-qa-db-fra.com

Meilleure façon de parcourir un tableau sans bloquer l'interface utilisateur

J'ai besoin d'itérer sur de grands tableaux et de les stocker dans des collections de dorsale à partir d'un appel d'API. Quelle est la meilleure façon de le faire sans faire en sorte que la boucle ne réagisse plus?

Le retour de la demande ajax est également bloqué car les données renvoyées sont si volumineuses. Je pense que je pourrais le scinder et utiliser setTimeout pour le faire fonctionner de manière asynchrone en morceaux plus petits, mais existe-t-il un moyen plus simple de le faire.

Je pensais qu'un employé Web serait bien, mais il doit modifier certaines structures de données enregistrées sur le fil de l'interface utilisateur. J'ai essayé d'utiliser ceci pour effectuer l'appel ajax, mais lorsqu'il renvoie les données au thread d'interface utilisateur, il est toujours temps que l'interface ne réponde plus.

Merci d'avance

49
georgephillips

Vous avez le choix avec ou sans webWorkers:

Sans WebWorkers

Pour le code devant interagir avec le DOM ou avec de nombreux autres états dans votre application, vous ne pouvez pas utiliser WebWorker. Par conséquent, la solution habituelle consiste à fractionner votre travail en morceaux. Chaque morceau de travail est programmé. La coupure entre les morceaux avec le minuteur permet au moteur de navigation de traiter d’autres événements en cours et permet non seulement de traiter les entrées de l’utilisateur, mais également de dessiner sur l’écran.

Habituellement, vous pouvez vous permettre d’en traiter plus d’un sur chaque minuterie, ce qui est plus efficace et plus rapide qu’un seul par minuterie. Ce code donne au thread d'interface utilisateur la possibilité de traiter tous les événements d'interface utilisateur en attente entre chaque bloc, ce qui maintiendra l'interface utilisateur active.

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);

Voici un exemple de travail du concept - pas cette même fonction, mais un processus long et différent qui utilise la même idée setTimeout() pour tester un scénario de probabilité avec de nombreuses itérations: http://jsfiddle.net/jfriend00/9hCVq/


Vous pouvez transformer ce qui précède en une version plus générique qui appelle une fonction de rappel telle que .forEach() fait comme ceci:

// 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);

Plutôt que de deviner le nombre de morceaux à la fois, il est également possible de laisser le temps écoulé être le guide de chaque morceau et de le traiter autant que possible dans un intervalle de temps donné. Cela garantit quelque peu automatiquement la réactivité du navigateur, quelle que soit la quantité d’itération utilisée par le processeur. Ainsi, plutôt que de passer à une taille de bloc, vous pouvez passer une valeur en millisecondes (ou simplement utiliser une valeur par défaut intelligente):

// 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);

Avec les WebWorkers

Si le code de votre boucle n'a pas besoin d'accéder au DOM, il est possible de mettre tout le code qui prend du temps dans un outil WebWorker. WebWorker s'exécutera indépendamment du navigateur principal Javascript et, lorsque ce sera fait, il pourra communiquer tous les résultats avec un postMessage.

Un outil WebWorker nécessite de séparer tout le code qui s'exécutera dans l'outil WebWorker dans un fichier de script distinct, mais il peut s'exécuter entièrement sans craindre le blocage du traitement d'autres événements dans le navigateur ni le "script ne répondant pas" peut apparaître lorsqu’un long processus est exécuté sur le thread principal.

75
jfriend00

Voici une démo de faire cette boucle "asynchrone". cela "retarde" l'itération pendant 1 ms et dans ce délai, cela donne à l'interface utilisateur une chance de faire quelque chose.

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

Il existe des alternatives telles que les travailleurs Web et le actuellement proposé setImmediate qui a toujours été, sur IE , avec un préfixe.

5
Joseph

Sur la base de @ jfriend00, voici une version prototype:

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