web-dev-qa-db-fra.com

Rafraîchissement DOM sur une fonction longue

J'ai un bouton qui exécute une longue fonction lorsque l'on clique dessus. Maintenant, pendant que la fonction est en cours d'exécution, je souhaite modifier le texte du bouton, mais des problèmes surviennent dans certains navigateurs tels que Firefox, IE.

html:

<button id="mybutt" class="buttonEnabled" onclick="longrunningfunction();"><span id="myspan">do some work</span></button>

javascript:

function longrunningfunction() {
    document.getElementById("myspan").innerHTML = "doing some work";
    document.getElementById("mybutt").disabled = true;
    document.getElementById("mybutt").className = "buttonDisabled";

    //long running task here

    document.getElementById("myspan").innerHTML = "done";
}

Maintenant, cela a des problèmes dans Firefox et IE, (en chrome cela fonctionne bien)

Alors j'ai pensé à le mettre dans un settimeout:

function longrunningfunction() {
    document.getElementById("myspan").innerHTML = "doing some work";
    document.getElementById("mybutt").disabled = true;
    document.getElementById("mybutt").className = "buttonDisabled";

    setTimeout(function() {
        //long running task here
        document.getElementById("myspan").innerHTML = "done";
    }, 0);
}

mais cela ne fonctionne pas non plus pour Firefox! le bouton est désactivé, change de couleur (en raison de l'application du nouveau css) mais le texte ne change pas.

Je dois régler le temps à 50ms au lieu de 0ms, afin de le faire fonctionner (changer le texte du bouton). Maintenant, je trouve ça au moins stupide. Je peux comprendre si cela fonctionnerait avec un délai de 0 ms seulement, mais que se passerait-il dans un ordinateur plus lent? peut-être que firefox aurait besoin de 100 ms dans le settimeout? cela semble plutôt stupide. J'ai essayé plusieurs fois, 1ms, 10ms, 20ms ... non, ça ne rafraîchira pas. seulement avec 50ms.

J'ai donc suivi les conseils sur ce sujet:

Forcer une actualisation du DOM dans Internet Explorer après une manipulation de javascript dom

alors j'ai essayé:

function longrunningfunction() {
    document.getElementById("myspan").innerHTML = "doing some work";
    var a = document.getElementById("mybutt").offsetTop; //force refresh

    //long running task here

    document.getElementById("myspan").innerHTML = "done";
}

mais ça ne marche pas (FIREFOX 21). Puis j'ai essayé:

function longrunningfunction() {
    document.getElementById("myspan").innerHTML = "doing some work";
    document.getElementById("mybutt").disabled = true;
    document.getElementById("mybutt").className = "buttonDisabled";
    var a = document.getElementById("mybutt").offsetTop; //force refresh
    var b = document.getElementById("myspan").offsetTop; //force refresh
    var c = document.getElementById("mybutt").clientHeight; //force refresh
    var d = document.getElementById("myspan").clientHeight; //force refresh

    setTimeout(function() {
        //long running task here
        document.getElementById("myspan").innerHTML = "done";
    }, 0);
}

J'ai même essayé clientHeight au lieu de offsetTop mais rien. le DOM n'est pas rafraîchi. 

Quelqu'un peut-il offrir une solution fiable de préférence non-hacky?

merci d'avance!

comme suggéré ici j'ai aussi essayé

$('#parentOfElementToBeRedrawn').hide().show();

en vain

Force le rafraîchissement/rafraîchissement du DOM sur Chrome/Mac

TL; DR:

rechercher une méthode multi-navigateurs FIABLE pour avoir un rafraîchissement DOM forcé SANS utilisation de setTimeout sur la situation)

jsfiddle: http://jsfiddle.net/WsmUh/5/

22
MIrrorMirror

RÉSOLU !! pas de setTimeout () !!!

Testé sous Chrome 27.0.1453, Firefox 21.0, Internet 9.0.8112

$("#btn").on("mousedown",function(){
$('#btn').html('working');}).on('mouseup', longFunc);

function longFunc(){
  //Do your long running work here...
   for (i = 1; i<1003332300; i++) {}
  //And on finish....  
   $('#btn').html('done');
}

DEMO ICI!

6
Kylie

Les pages Web sont mises à jour sur la base d’un seul contrôleur de thread, et la moitié des navigateurs ne mettent pas à jour le DOM ou le style jusqu’à ce que votre exécution JS s’arrête, ce qui rend le contrôle informatique au navigateur. Cela signifie que si vous définissez un element.style.[...] = ..., il ne sera pas activé tant que votre code ne sera pas exécuté (soit complètement, soit parce que le navigateur voit que vous faites quelque chose qui lui permet d'intercepter le traitement pendant quelques ms).

Vous avez deux problèmes: 1) votre bouton contient un <span>. Supprimez cela, définissez simplement .innerHTML sur le bouton lui-même. Mais ce n'est pas le vrai problème bien sûr. 2) vous menez de très longues opérations et vous devriez réfléchir très sérieusement au pourquoi, et après avoir répondu au pourquoi, comment:

Si vous exécutez un travail de calcul long, découpez-le en rappels de délai d'attente. Vos exemples ne montrent pas ce qu'est réellement votre "long travail" (une boucle de rotation ne compte pas) mais vous avez plusieurs options selon les navigateurs que vous utilisez, avec un G&EACUTE;ANT booknote: don ' t exécuter de longues tâches en JavaScript, point final. JavaScript est un environnement à thread unique par spécification. Par conséquent, toute opération que vous souhaitez effectuer doit pouvoir être effectuée en quelques millisecondes. Si cela ne peut pas, vous faites littéralement quelque chose de mal.

Si vous devez calculer des tâches difficiles, transférez-les au serveur avec une opération AJAX (universelle sur tous les navigateurs, ce qui vous donne souvent a) un traitement plus rapide pour cette opération et b) 30 bonnes secondes au minimum. de manière asynchrone, attendez que le résultat soit renvoyé) ou utilisez un fil d’arrière-plan de travailleur Web (beaucoup, PAS universel). 

Si votre calcul prend beaucoup de temps, mais pas de façon absurde, reformulez votre code de manière à ce que vous exécutiez des pièces, avec un temps mort expiratoire:

function doLongCalculation(callbackFunction) {
  var partialResult = {};
  // part of the work, filling partialResult
  setTimeout(function(){ doSecondBit(partialResult, callbackFunction); }, 10);
}

function doSecondBit(partialResult, callbackFunction) {
  // more 'part of the work', filling partialResult
  setTimeout(function(){ finishUp(partialResult, callbackFunction); }, 10);
}

function finishUp(partialResult, callbackFunction) {
  var result;
  // do last bits, forming final result
  callbackFunction(result);
}

Un calcul long peut presque toujours être reformulé en plusieurs étapes, soit parce que vous effectuez plusieurs étapes, soit parce que vous exécutez le même calcul un million de fois et que vous pouvez le diviser en lots. Si vous avez (exagéré) ceci:

var resuls = [];
for(var i=0; i<1000000; i++) {
  // computation is performed here
  if(...) results.Push(...);
}

alors vous pouvez trivialement couper cela en une fonction de relaxation avec un rappel

function runBatch(start, end, terminal, results, callback) {
  var i;
  for(var i=start; i<end; i++) {
    // computation is performed here
    if(...) results.Push(...);      } 
  if(i>=terminal) {
    callback(results);
  } else {
    var inc = end-start;
    setTimeout(function() {
      runBatch(start+inc, end+inc, terminal, results, callback);
    },10);
  }
}

function dealWithResults(results) {
  ...
}

function doLongComputation() {
  runBatch(0,1000,1000000,[],dealWithResults);
}

TL; DR : ne lancez pas de longs calculs, mais si vous devez le faire, laissez le serveur faire le travail pour vous et utilisez simplement un appel AJAX asynchrone. Le serveur peut faire le travail plus rapidement et votre page ne bloquera pas.

Les exemples de JS sur la manière de traiter des calculs longs dans JS chez le client ne sont là que pour expliquer comment vous pouvez traiter ce problème si vous n’avez pas la possibilité de faire des appels AJAX, dont 99,99% des le temps ne sera pas le cas.

edit

notez également que votre description de prime est un cas classique de Le problème XY

14

Comme décrit dans la section "Script prenant des tâches trop longues et lourdes" de Événements et minutage en profondeur (une lecture intéressante, en passant):

[...] divise le travail en parties qui sont planifiées les unes après les autres. [...] Ensuite, il y a un «temps libre» pour que le navigateur réponde entre les parties. C'est peut rendre et réagir sur d'autres événements. Le visiteur et le navigateur sont contents.

Je suis sûr qu'il y a de nombreuses fois où une tâche ne peut pas être scindée en tâches plus petites ou en fragments. Mais je suis sûr qu’il y aura bien d’autres occasions où cela sera possible! :-)

Une refactorisation est nécessaire dans l'exemple fourni. Vous pourriez créer une fonction pour effectuer une partie du travail vous devez le faire. Ça pourrait commencer comme ça:

function doHeavyWork(start) {
    var total = 1000000000;
    var fragment = 1000000;
    var end = start + fragment;

    // Do heavy work
    for (var i = start; i < end; i++) {
        //
    }

Une fois le travail terminé, la fonction doit déterminer si le travail suivant doit être effectué ou si l'exécution est terminée:

    if (end == total) {
        // If we reached the end, stop and change status
        document.getElementById("btn").innerHTML = "done!";
    } else {
        // Otherwise, process next fragment
        setTimeout(function() {
            doHeavyWork(end);
        }, 0);
    }            
}

Votre fonction principale dowork () ressemblerait à ceci:

function dowork() {
    // Set "working" status
    document.getElementById("btn").innerHTML = "working";

    // Start heavy process
    doHeavyWork(0);
}

Code de travail complet sur http://jsfiddle.net/WsmUh/19/ (semble se comporter avec douceur sur Firefox).

3
German Latorre

Si vous ne souhaitez pas utiliser setTimeout, il vous reste alors WebWorker - cela nécessitera toutefois des navigateurs compatibles HTML5.

C’est une façon de les utiliser - 

Définissez votre code HTML et un script en ligne (vous n’avez pas à utiliser de script en ligne, vous pouvez également donner une URL à un fichier JS séparé existant):

<input id="start" type="button" value="Start" />
<div id="status">Preparing worker...</div>

<script type="javascript/worker">
    postMessage('Worker is ready...');

    onmessage = function(e) {
        if (e.data === 'start') {
            //simulate heavy work..
            var max = 500000000;
            for (var i = 0; i < max; i++) {
                if ((i % 100000) === 0) postMessage('Progress: ' + (i / max * 100).toFixed(0) + '%');
            }
            postMessage('Done!');
        }
    };
</script>

Pour le script en ligne, nous le marquons avec le type javascript/worker.

Dans le fichier Javascript normal -

Fonction qui convertit le script en ligne en une URL de type blob pouvant être transmise à un WebWorker. Notez que cela pourrait être utile dans IE et que vous devrez utiliser un fichier normal:

function getInlineJS() {
    var js = document.querySelector('[type="javascript/worker"]').textContent;
    var blob = new Blob([js], {
        "type": "text\/plain"
    });
    return URL.createObjectURL(blob);
}

Ouvrier d'installation:

var ww = new Worker(getInlineJS());

Recevoir des messages (ou des commandes) de la WebWorker:

ww.onmessage = function (e) {
    var msg = e.data;

    document.getElementById('status').innerHTML = msg;

    if (msg === 'Done!') {
        alert('Next');    
    }
};

Nous démarrons avec un clic de bouton dans cette démo:

document.getElementById('start').addEventListener('click', start, false);

function start() {
    ww.postMessage('start');
}

Exemple de travail ici:
http://jsfiddle.net/AbdiasSoftware/Ls4XJ/

Comme vous pouvez le constater, l'interface utilisateur est mise à jour (avec la progression dans cet exemple) même si nous utilisons une boucle-occupé sur le travailleur. Cela a été testé avec un ordinateur Atom (lent).

Si vous ne voulez pas ou ne pouvez pas utiliser WebWorker, vous avez à utiliser setTimeout.

C'est parce que c'est le seul moyen (à part de setInterval) qui vous permet de mettre en file d'attente un événement. Comme vous l'avez remarqué, vous devrez lui donner quelques millisecondes, ce qui donnera au moteur de l'interface utilisateur "suffisamment de place pour se reproduire" pour ainsi dire. Comme JS est à thread unique, vous ne pouvez pas mettre les événements en file d'attente d'une autre manière (requestAnimationFrame ne fonctionnera pas bien dans ce contexte).

J'espère que cela t'aides.

2
user1693593

Mise à jour: je ne pense pas à long terme que vous puissiez être sûr d'éviter l'évitement agressif de Firefox des mises à jour DOM sans utiliser de délai d'expiration. Si vous voulez forcer une mise à jour/mise à jour DOM redessinée, des astuces sont disponibles, comme ajuster le décalage des éléments, ou faire hide () puis show (), etc., mais rien n’est très joli, et après un moment, lorsque les astuces deviennent abusées et ralentissent l'expérience utilisateur, puis les navigateurs sont mis à jour pour ignorer ces astuces. Voir cet article et les articles liés à côté pour des exemples: Forcer le rafraîchissement/rafraîchissement du DOM sur Chrome/Mac

Les autres réponses semblent contenir les éléments de base nécessaires, mais j’ai pensé qu’il serait utile de mentionner que ma pratique est d’envelopper toutes les fonctions interactives de changement de DOM dans une fonction "dispatch" qui gère les pauses nécessaires pour contourner le fait Firefox s’avère extrêmement agressif en évitant les mises à jour DOM afin de marquer de bons points de repère (et d’être réactif face aux utilisateurs tout en naviguant sur Internet).

J'ai jeté un coup d'œil à votre JSFiddle et personnalisé une fonction de répartition, fonction sur laquelle reposent bon nombre de mes programmes. Je pense que cela va de soi, et vous pouvez simplement le coller dans votre JS Fiddle existant pour voir comment cela fonctionne:

$("#btn").on("click", function() { dispatch(this, dowork, 'working', 'done!'); });

function dispatch(me, work, working, done) {
    /* work function, working message HTML string, done message HTML string */
    /* only designed for a <button></button> element */
    var pause = 50, old;
    if (!me || me.tagName.toLowerCase() != 'button' || me.innerHTML == working) return;
    old = me.innerHTML;
    me.innerHTML = working;
    setTimeout(function() {
        work();
        me.innerHTML = done;
        setTimeout(function() { me.innerHTML = old; }, 1500);
    }, pause);
}

function dowork() {
        for (var i = 1; i<1000000000; i++) {
            //
        }

}

Remarque: la fonction de répartition bloque également les appels en même temps, car elle peut créer de la confusion parmi les utilisateurs si les mises à jour de l'état de plusieurs clics se produisent simultanément.

1
Joseph Myers

Modifiez légèrement votre code sur jsfiddle et:

$("#btn").on("click", dowork);

function dowork() {
    document.getElementById("btn").innerHTML = "working";
    setTimeout(function() {
        for (var i = 1; i<1000000000; i++) {
            //
        }
        document.getElementById("btn").innerHTML = "done!";
    }, 100);
}

Le délai d'attente défini sur une valeur plus raisonnable 100ms a été efficace pour moi. Essayez. Essayez d’ajuster la latence pour trouver la meilleure valeur.

0
ElmoVanKielmo

Le tampon DOM existe également dans le navigateur par défaut d'Android, JavaScript de longue durée ne videra le tampon DOM qu'une seule fois, Utilise setTimeout (..., 50) pour le résoudre.

0
diyism

Faux une requête ajax

function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
$.ajax({
    url: "/",
    complete: function () {
        //long running task here
        document.getElementById("myspan").innerHTML = "done";
    }
});}
0
TeamEASI.com

Certains navigateurs ne gèrent pas l'attribut onclick html correct. Il est préférable d'utiliser cet événement sur l'objet js. Comme ça:

<button id="mybutt" class="buttonEnabled">
    <span id="myspan">do some work</span>
</button>

<script type="text/javascript">

window.onload = function(){ 

    butt = document.getElementById("mybutt");
    span = document.getElementById("myspan");   

    butt.onclick = function () {
        span.innerHTML = "doing some work";
        butt.disabled = true;
        butt.className = "buttonDisabled";

        //long running task here

        span.innerHTML = "done";
    };

};

</script>

J'ai fait du violon avec l'exemple de travail http://jsfiddle.net/BZWbH/2/

0
Goran Lepur

Avez-vous essayé d'ajouter l'auditeur à "onmousedown" pour changer le texte du bouton et cliquer sur l'événement pour une fonction longue durée.

0
user1390282

Essaye ça

function longRunningTask(){
    // Do the task here
    document.getElementById("mybutt").value = "done";
}

function longrunningfunction() {
    document.getElementById("mybutt").value = "doing some work";

    setTimeout(function() {
        longRunningTask();
    }, 1);    
}
0
Jay Patel