web-dev-qa-db-fra.com

Comment faire du code javascript non bloquant?

Comment puis-je effectuer un simple appel de fonction Javascript sans blocage? 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);
  }

les sorties

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

Comment puis-je former cette boucle simple à exécuter de manière 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 s'appuyer sur des bibliothèques ou des fonctions externes. Aucun d'entre eux ne répond à la question en vase clos: comment écrit-on un code Javascript non bloquant!?


J'ai cherché très fort cette réponse avant de demander; s'il vous plaît ne supposez pas que je n'ai pas regardé. Tout ce que j'ai trouvé est spécifique à Node.js ( [1] , [2] , [3] , [4] , [5] ) ou autrement spécifique à d'autres fonctions ou bibliothèques ( [6] , [7] , [8] , [9] , [10] , [11] ), notamment JQuery et setTimeout(). Aidez-moi à écrire du code non bloquant en utilisant Javascript, et non des outils écrits en Javascript, comme JQuery et Node. Veuillez relire la question avant de la marquer en double. 

23
user1717828

SetTimeout avec des rappels est le chemin à parcourir. Cependant, comprenez que les portées de vos fonctions ne sont pas les mêmes que celles de C # ou d’un autre environnement multi-thread.

Javascript n'attend pas le rappel de votre fonction pour se terminer.

Si tu le dis:

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

Votre alerte sera déclenchée avant la fonction que vous avez passée.

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

5
Andrew Hoffman

Pour que votre boucle ne soit pas bloquante, vous devez la diviser en sections et permettre à la boucle de traitement des événements JS de consommer des événements utilisateur avant de passer à la section suivante.

Pour ce faire, le moyen le plus simple consiste à effectuer un certain travail, puis à utiliser setTimeout(..., 0) pour mettre le travail suivant en file d'attente. De manière cruciale, cette mise en file d'attente permet à la boucle d'événements JS de traiter tous les événements mis en attente entre-temps avant de passer au travail suivant:

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émonstration où la fonction callback définit simplement une variable sur le nombre d'itérations en cours, et une boucle séparée basée sur setTimeout interroge la valeur actuelle de cette variable et met à jour la page avec sa valeur.

19
Alnitak

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

Alors, cela ne fonctionnera 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 des multi-threads, vous devez utiliser Web Workers , mais ils doivent avoir un fichier js séparé et vous ne pouvez que leur transmettre des objets.

Mais, J'ai réussi à utiliser Web Workers sans fichiers séparés en générant des fichiers Blob et je peux également leur transmettre des fonctions de rappel.

//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
Fernando Carvajal

Autant que je sache, il y a généralement deux façons de le faire. L'une consiste à utiliser setTimeout (ou requestAnimationFrame si vous le faites dans un environnement de support). @Alnitak a montré comment faire cela dans une autre réponse. Une autre méthode consiste à utiliser un outil de traitement Web pour terminer votre logique de blocage dans un thread séparé, afin que le thread d'interface utilisateur principal ne soit pas bloqué. 

Utiliser 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();
}

Utiliser un travailleur Web:

/***** 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 travail Web, car elle nécessite un fichier worker.js séparé dans la logique de travail de l'hôte.

1
Ryan.C

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

}

Ensuite, vous pourrez utiliser la promesse retournée pour gérer les progrès et le rappel effectué:

var testArray =["Banana", "Orange", "Apple", "Mango"];
deferredEach(testArray, 2).progress(function(index, item){
    alert(item);
}).done(function(){
    alert("Done!");
})
0
The_Black_Smurf