web-dev-qa-db-ger.com

Wie kann man einen nicht blockierenden Javascript-Code erstellen?

Wie kann ich einen einfachen, nicht blockierenden Javascript-Funktionsaufruf durchführen? Zum Beispiel:

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

ausgänge

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

Wie kann ich diese einfache Schleife so erstellen, dass sie asynchron ausgeführt wird und die Ergebnisse über eine Rückruffunktion ausgeben? Die Idee ist, nicht "mehr Sachen machen" zu blockieren:

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

Ich habe folgende Tutorials zu Rückrufen und Fortsetzungen ausprobiert, aber alle scheinen sich auf externe Bibliotheken oder Funktionen zu verlassen. Keiner von ihnen beantwortet die Frage im luftleeren Raum: Wie schreibt man Javascript-Code, um nicht blockierend zu sein?


Ich habe sehr intensiv nach dieser Antwort gesucht, bevor ich fragte; bitte nicht davon ausgehen, dass ich nicht geschaut habe. Alles was ich gefunden habe ist Node.js spezifisch ( [1] , [2] , [3] , [4] , [5] ) oder ansonsten spezifisch für andere Funktionen oder Bibliotheken ( [6] , [7] , [8] , [9] , [10] , [11] ), insbesondere JQuery und setTimeout(). Bitte helfen Sie mir, nicht-blockierenden Code mit Javascript zu schreiben, nicht mit Javascript geschriebenen Tools wie JQuery und Node. Lesen Sie die Frage bitte erneut, bevor Sie sie als Duplikat markieren. 

23
user1717828

SetTimeout mit Callbacks ist der Weg zu gehen. Verstehen Sie jedoch, dass Ihre Funktionsbereiche nicht mit C # oder einer anderen Multithread-Umgebung identisch sind.

Javascript wartet nicht, bis der Rückruf Ihrer Funktion abgeschlossen ist.

Wenn du sagst:

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

Ihre Warnung wird ausgelöst, bevor die von Ihnen übergebene Funktion ausgeführt wird.

Der Unterschied besteht darin, dass die Warnung den Thread blockiert hat, Ihr Callback jedoch nicht.

5
Andrew Hoffman

Damit Ihre Schleife nicht blockiert, müssen Sie sie in Abschnitte unterteilen und zulassen, dass die JS-Ereignisverarbeitungsschleife Benutzerereignisse verarbeitet, bevor Sie mit dem nächsten Abschnitt fortfahren.

Dies erreichen Sie am einfachsten, indem Sie eine bestimmte Menge Arbeit erledigen und dann mit setTimeout(..., 0) den nächsten Teil der Arbeit in die Warteschlange stellen. Diese Warteschlange ermöglicht es der JS-Ereignisschleife, Ereignisse zu verarbeiten, die sich in der Zwischenzeit in der Warteschlange befanden, bevor sie mit der nächsten Arbeit fortfährt:

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

bei Verwendung:

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

Siehe http://jsfiddle.net/alnitak/x3bwjjo6/ für eine Demo, bei der die callback-Funktion eine Variable nur auf den aktuellen Iterationszähler setzt und eine separate setTimeout-basierte Schleife den aktuellen Wert dieser Variablen abruft und den Wert aktualisiert Seite mit ihrem Wert.

19
Alnitak

Sie können nicht zwei Schleifen gleichzeitig ausführen. Denken Sie daran, dass JS ein einzelner Thread ist.

Also wird das nie funktionieren

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)

Wenn Sie Multithread erreichen möchten, müssen Sie Web Workers verwenden. Diese müssen jedoch eine separate js-Datei haben und Sie können nur Objekte an sie übergeben.

Ich habe es geschafft, Web Workers zu verwenden ohne getrennte Dateien, indem ich Blob-Dateien generierte, und ich kann ihnen auch Callback-Funktionen übergeben.

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

Es gibt im Allgemeinen zwei Möglichkeiten, dies zu tun, soweit ich weiß. Verwenden Sie setTimeout (oder requestAnimationFrame, wenn Sie dies in einer unterstützenden Umgebung tun). @Alnitak hat in einer anderen Antwort gezeigt, wie das geht. Eine andere Möglichkeit besteht darin, einen Web-Worker zu verwenden, um Ihre Blockierungslogik in einem separaten Thread zu beenden, sodass der Haupt-UI-Thread nicht blockiert wird. 

Verwenden Sie requestAnimationFrame oder 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();
}

Verwendung von Web Worker:

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

Sie können die Web-Worker-Lösung nicht ausführen, da für Host-Worker-Logik eine separate worker.js-Datei erforderlich ist.

1
Ryan.C

Wenn Sie jQuery verwenden, habe ich eine verzögerte Implementierung von Alnitaks Antwort erstellt.

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

}

Dann können Sie das zurückgegebene Versprechen verwenden, um den Fortschritt und den durchgeführten Rückruf zu verwalten:

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