Comment faire du code javascript non-bloquant?

Comment puis-je faire un simple appel de fonction Javascript sans bloc? Par exemple:

  //begin the program
  console.log('begin');
  nonBlockingIncrement(10000000);
  console.log('do more stuff'); 

  //define the slow function; this would normally be a server call
  function nonBlockingIncrement(n){
    var i=0;
    while(i<n){
      i++;
    }
    console.log('0 incremented to '+i);
  }

sorties

"beginPage" 
"0 incremented to 10000000"
"do more stuff"

Comment puis-je former cette boucle simple pour exécuter asynchrone et afficher les résultats via une fonction de rappel? L'idée est de ne pas bloquer "faire plus de choses":

"beginPage" 
"do more stuff"
"0 incremented to 10000000"

j'ai essayé de suivre des tutoriels sur les callbacks et les continuations, mais ils semblent tous compter sur des bibliothèques ou des fonctions externes. Aucun d'entre eux ne répond à la question dans le vide: Comment écrire du code Javascript pour être non-bloquant!?


j'ai cherché très fort cette réponse avant de la Demander; ne présumez pas que je n'ai pas cherché. Tout ce que j'ai trouvé est un noeud.js specific ( [1] , [2] , [3] , [4] , [5] ) ou autrement spécifique à autres fonctions ou bibliothèques ( [6] , [7] , [8] , [9] , [10] , [11] ), notamment JQuery et setTimeout() . S'il vous plaît aidez-moi à écrire le code non-bloquant en utilisant Javascript , pas les outils Javascript-écrits comme jQuery et noeud. veuillez relire la question avant de la marquer comme une copie.

23
demandé sur Community 2014-10-28 21:36:48

5 réponses

SetTimeout avec des rappels est la solution. Bien que, comprendre vos portées de fonction ne sont pas les mêmes que dans C# ou un autre environnement multi-threaded.

Javascript n'attend pas la fin du rappel de votre fonction.

si vous dites:

function doThisThing(theseArgs) {
    setTimeout(function (theseArgs) { doThatOtherThing(theseArgs); }, 1000);
    alert('hello world');
}

votre alerte se déclenchera avant que la fonction que vous avez passée ne se déclenche.

La différence étant que l'alerte a bloqué le fil, mais votre rappel n' pas.

5
répondu Andrew Hoffman 2014-10-28 18:55:13

pour rendre votre boucle non-bloquante, vous devez la découper en sections et permettre à la boucle de traitement d'événements JS de consommer les événements Utilisateur avant de passer à la section suivante.

la façon la plus simple d'y parvenir est de faire un certain travail, puis d'utiliser setTimeout(..., 0) pour faire la file d'attente du prochain morceau de travail. Ce qui est crucial, c'est que la file d'attente permet à la boucle d'événements JS de traiter tous les événements qui ont été mis en file d'attente entre-temps avant de passer à l'étape suivante du travail.:

function yieldingLoop(count, chunksize, callback, finished) {
    var i = 0;
    (function chunk() {
        var end = Math.min(i + chunksize, count);
        for ( ; i < end; ++i) {
            callback.call(null, i);
        }
        if (i < count) {
            setTimeout(chunk, 0);
        } else {
            finished.call(null);
        }
    })();
}

avec usage:

yieldingLoop(1000000, 1000, function(i) {
    // use i here
}, function() {
    // loop done here
});

Voir http://jsfiddle.net/alnitak/x3bwjjo6/ pour une démo où le callback fonction définit une variable à l'itération courante de comptage, et un autre setTimeout boucles interroge la valeur courante de cette variable et les mises à jour de la page avec sa valeur.

18
répondu Alnitak 2014-10-28 19:03:52

vous ne pouvez pas exécuter deux boucles en même temps, rappelez-vous que JS est un fil simple.

ainsi, faire cela ne marchera jamais

function loopTest() {
    var test = 0
    for (var i; i<=100000000000, i++) {
        test +=1
    }
    return test
}

setTimeout(()=>{
    //This will block everything, so the second won't start until this loop ends
    console.log(loopTest()) 
}, 1)

setTimeout(()=>{
    console.log(loopTest())
}, 1)

si vous voulez réaliser le multi thread vous devez utiliser les travailleurs Web , mais ils doivent avoir un fichier JS séparé et vous ne pouvez passer des objets à eux.

mais, j'ai réussi à utiliser des travailleurs Web sans les fichiers séparés en générant des fichiers Blob et je peux leur passer des fonctions de callback aussi.

//A fileless Web Worker
class ChildProcess {
     //@param {any} ags, Any kind of arguments that will be used in the callback, functions too
    constructor(...ags) {
        this.args = ags.map(a => (typeof a == 'function') ? {type:'fn', fn:a.toString()} : a)
    }

    //@param {function} cb, To be executed, the params must be the same number of passed in the constructor 
    async exec(cb) {
        var wk_string = this.worker.toString();
        wk_string = wk_string.substring(wk_string.indexOf('{') + 1, wk_string.lastIndexOf('}'));            
        var wk_link = window.URL.createObjectURL( new Blob([ wk_string ]) );
        var wk = new Worker(wk_link);

        wk.postMessage({ callback: cb.toString(), args: this.args });
 
        var resultado = await new Promise((next, error) => {
            wk.onmessage = e => (e.data && e.data.error) ? error(e.data.error) : next(e.data);
            wk.onerror = e => error(e.message);
        })

        wk.terminate(); window.URL.revokeObjectURL(wk_link);
        return resultado
    }

    worker() {
        onmessage = async function (e) {
            try {                
                var cb = new Function(`return ${e.data.callback}`)();
                var args = e.data.args.map(p => (p.type == 'fn') ? new Function(`return ${p.fn}`)() : p);

                try {
                    var result = await cb.apply(this, args); //If it is a promise or async function
                    return postMessage(result)

                } catch (e) { throw new Error(`CallbackError: ${e}`) }
            } catch (e) { postMessage({error: e.message}) }
        }
    }
}

setInterval(()=>{console.log('Not blocked code ' + Math.random())}, 1000)

console.log("starting blocking synchronous code in Worker")
console.time("\nblocked");

var proc = new ChildProcess(blockCpu, 43434234);

proc.exec(function(block, num) {
    //This will block for 10 sec, but 
    block(10000) //This blockCpu function is defined below
    return `\n\nbla bla ${num}\n` //Captured in the resolved promise
}).then(function (result){
    console.timeEnd("\nblocked")
    console.log("End of blocking code", result)
})
.catch(function(error) { console.log(error) })

//random blocking function
function blockCpu(ms) {
    var now = new Date().getTime();
    var result = 0
    while(true) {
        result += Math.random() * Math.random();
        if (new Date().getTime() > now +ms)
            return;
    }   
}
1
répondu Fernando Carvajal 2018-01-23 02:52:45

Il y a en général deux façons de le faire autant que je sache. L'une est d'utiliser setTimeout (ou requestAnimationFrame si vous faites cela dans un environnement de support). @Alnitak a montré comment faire cela dans une autre réponse. Une autre façon est d'utiliser un travailleur web pour terminer votre logique de blocage dans un thread séparé, de sorte que le thread principal de L'interface utilisateur n'est pas bloqué.

par requestAnimationFrame ou setTimeout :

//begin the program
console.log('begin');
nonBlockingIncrement(100, function (currentI, done) {
  if (done) {
    console.log('0 incremented to ' + currentI);
  }
});
console.log('do more stuff'); 

//define the slow function; this would normally be a server call
function nonBlockingIncrement(n, callback){
  var i = 0;
  
  function loop () {
    if (i < n) {
      i++;
      callback(i, false);
      (window.requestAnimationFrame || window.setTimeout)(loop);
    }
    else {
      callback(i, true);
    }
  }
  
  loop();
}

à l'Aide de web travailleur:

/***** Your worker.js *****/
this.addEventListener('message', function (e) {
  var i = 0;

  while (i < e.data.target) {
    i++;
  }

  this.postMessage({
    done: true,
    currentI: i,
    caller: e.data.caller
  });
});



/***** Your main program *****/
//begin the program
console.log('begin');
nonBlockingIncrement(100, function (currentI, done) {
  if (done) {
    console.log('0 incremented to ' + currentI);
  }
});
console.log('do more stuff'); 

// Create web worker and callback register
var worker = new Worker('./worker.js'),
    callbacks = {};

worker.addEventListener('message', function (e) {
  callbacks[e.data.caller](e.data.currentI, e.data.done);
});

//define the slow function; this would normally be a server call
function nonBlockingIncrement(n, callback){
  const caller = 'nonBlockingIncrement';
  
  callbacks[caller] = callback;
  
  worker.postMessage({
    target: n,
    caller: caller
  });
}

vous ne pouvez pas exécuter la solution de travailleur web car elle nécessite un fichier worker.js séparé pour la logique de travailleur hôte.

1
répondu Ryan.C 2018-05-07 19:31:58

si vous utilisez jQuery, j'ai créé une implémentation différée de réponse D'Alnitak

function deferredEach (arr, batchSize) {

    var deferred = $.Deferred();

    var index = 0;
    function chunk () {
        var lastIndex = Math.min(index + batchSize, arr.length);

        for(;index<lastIndex;index++){
            deferred.notify(index, arr[index]);
        }

        if (index >= arr.length) {
            deferred.resolve();
        } else {
            setTimeout(chunk, 0);
        }
    };

    setTimeout(chunk, 0);

    return deferred.promise();

}

alors vous pourrez utiliser la promesse retournée pour gérer la progression et le rappel fait:

var testArray =["Banana", "Orange", "Apple", "Mango"];
deferredEach(testArray, 2).progress(function(index, item){
    alert(item);
}).done(function(){
    alert("Done!");
})
0
répondu The_Black_Smurf 2018-10-04 20:13:50