Ich habe mich einen Tag lang mit den Lösungen beschäftigt, aber ich denke immer noch darüber nach, wie ich die Verkettbarkeit bei der Verwendung eines Rückrufs aufrechterhalten kann.
Jeder ist mit dem traditionellen Programmierstil vertraut, bei dem der Code Zeile für Zeile synchronisiert wird. SetTimeout verwendet einen Rückruf, so dass die nächste Zeile nicht auf ihre Fertigstellung wartet. Dies lässt mich darüber nachdenken, wie man es "synchronisieren" kann, um eine "Sleep"-Funktion zu erstellen.
Beginnend mit einer einfachen Coroutine:
function coroutine() {
console.log('coroutine-1:start');
sleepFor(3000); // Sleep for 3 seconds here
console.log('coroutine-2:complete');
}
Ich möchte in der Mitte 3 Sekunden schlafen, aber ich möchte nicht den gesamten Ablauf dominieren, also muss die Coroutine von einem anderen Thread ausgeführt werden. Ich betrachte die Unity YieldInstruction und ändern Sie die Koroutine wie folgt:
function coroutine1() {
this.a = 100;
console.log('coroutine1-1:start');
return sleepFor(3000).yield; // Sleep for 3 seconds here
console.log('coroutine1-2:complete');
this.a++;
}
var c1 = new coroutine1();
Deklarieren Sie den sleepFor-Prototyp:
sleepFor = function(ms) {
var caller = arguments.callee.caller.toString();
var funcArgs = /\(([\s\S]*?)\)/gi.exec(caller)[1];
var args = arguments.callee.caller.arguments;
var funcBody = caller.replace(/^[\s\S]*?sleepFor[\s\S]*?yield;|}[\s;]*$/g,'');
var context = this;
setTimeout(function() {
new Function(funcArgs, funcBody).apply(context, args);
}, ms);
return this;
}
Nach dem Ausführen der Coroutine1 (ich habe sie in Internet Explorer 11 und Chrome 49 getestet), werden Sie sehen, dass sie zwischen zwei Konsolenanweisungen 3 Sekunden schläft. Es hält die Codes so schön wie der traditionelle Stil.
Das Knifflige liegt in der sleepFor-Routine. Sie liest den Körper der aufrufenden Funktion als String und zerlegt ihn in zwei Teile. Der obere Teil wird entfernt und mit dem unteren Teil eine weitere Funktion erstellt. Nachdem sie die angegebene Anzahl von Millisekunden gewartet hat, ruft sie die erstellte Funktion unter Verwendung des ursprünglichen Kontexts und der ursprünglichen Argumente auf. Der ursprüngliche Ablauf wird wie üblich mit "return" beendet. Für das "yield"? Es wird für den Abgleich mit regulären Ausdrücken verwendet. Er ist notwendig, aber nicht von Nutzen.
Sie ist nicht zu 100 % perfekt, aber sie erfüllt zumindest meine Aufgaben. Ich muss einige Einschränkungen bei der Verwendung dieses Codes erwähnen. Da der Code in zwei Teile aufgeteilt ist, muss die "return"-Anweisung in der äußeren Anweisung stehen, anstatt in einer Schleife oder {}. d.h.
function coroutine3() {
this.a = 100;
console.log('coroutine3-1:start');
if(true) {
return sleepFor(3000).yield;
} // <- Raise an exception here
console.log('coroutine3-2:complete');
this.a++;
}
Der obige Code muss ein Problem haben, da die schließende Klammer nicht einzeln in der erstellten Funktion existieren kann. Eine weitere Einschränkung ist, dass alle lokalen Variablen, die mit "var xxx=123" deklariert werden, nicht in die nächste Funktion übernommen werden können. Sie müssen "this.xxx=123" verwenden, um das Gleiche zu erreichen. Wenn Ihre Funktion Argumente hat und diese geändert werden, kann der geänderte Wert auch nicht in die nächste Funktion übertragen werden.
function coroutine4(x) { // Assume x=abc
var z = x;
x = 'def';
console.log('coroutine4-1:start' + z + x); // z=abc, x=def
return sleepFor(3000).yield;
console.log('coroutine4-2:' + z + x); // z=undefined, x=abc
}
Ich würde einen weiteren Funktionsprototyp einführen: waitFor
waitFor = function(check, ms) {
var caller = arguments.callee.caller.toString();
var funcArgs = /\(([\s\S]*?)\)/gi.exec(caller)[1];
var args = arguments.callee.caller.arguments;
var funcBody = caller.replace(/^[\s\S]*?waitFor[\s\S]*?yield;|}[\s;]*$/g,'');
var context = this;
var thread = setInterval(function() {
if(check()) {
clearInterval(thread);
new Function(funcArgs, funcBody).apply(context, args);
}
}, ms?ms:100);
return this;
}
Es wartet auf die Funktion "check", bis sie true zurückgibt. Sie prüft den Wert alle 100 ms. Sie können sie durch Übergabe eines zusätzlichen Arguments anpassen. Betrachten Sie die Test-Coroutine2:
function coroutine2(c) {
/* Some code here */
this.a = 1;
console.log('coroutine2-1:' + this.a++);
return sleepFor(500).yield;
/* Next */
console.log('coroutine2-2:' + this.a++);
console.log('coroutine2-2:waitFor c.a>100:' + c.a);
return waitFor(function() {
return c.a>100;
}).yield;
/* The rest of the code */
console.log('coroutine2-3:' + this.a++);
}
Auch in dem hübschen Stil, den wir bisher so lieben. Eigentlich hasse ich den verschachtelten Callback. Es ist leicht verständlich, dass die Coroutine2 auf den Abschluss von Coroutine1 wartet. Interessant? Ok, dann führen Sie den folgenden Code aus:
this.a = 10;
console.log('outer-1:' + this.a++);
var c1 = new coroutine1();
var c2 = new coroutine2(c1);
console.log('outer-2:' + this.a++);
Die Ausgabe ist:
outer-1:10
coroutine1-1:start
coroutine2-1:1
outer-2:11
coroutine2-2:2
coroutine2-2:waitFor c.a>100:100
coroutine1-2:complete
coroutine2-3:3
Outer wird unmittelbar nach der Initialisierung von coroutine1 und coroutine2 abgeschlossen. Dann wartet Coroutine1 3000 ms lang. Coroutine2 beginnt nach einer Wartezeit von 500 ms mit Schritt 2. Danach fährt sie mit Schritt 3 fort, sobald sie feststellt, dass die Werte von coroutine1.a > 100 sind.
Beachten Sie, dass es drei Kontexte für die Variable "a" gibt. Einer ist outer, die Werte sind 10 und 11. Ein anderer ist in coroutine1, die Werte sind 100 und 101. Der letzte ist in coroutine2, die Werte sind 1, 2 und 3. In der Coroutine2 wird auch auf c.a aus der Coroutine1 gewartet, bis der Wert größer als 100 ist. Die 3 Kontexte sind unabhängig.
Der gesamte Code für Copy&Paste:
sleepFor = function(ms) {
var caller = arguments.callee.caller.toString();
var funcArgs = /\(([\s\S]*?)\)/gi.exec(caller)[1];
var args = arguments.callee.caller.arguments;
var funcBody = caller.replace(/^[\s\S]*?sleepFor[\s\S]*?yield;|}[\s;]*$/g,'');
var context = this;
setTimeout(function() {
new Function(funcArgs, funcBody).apply(context, args);
}, ms);
return this;
}
waitFor = function(check, ms) {
var caller = arguments.callee.caller.toString();
var funcArgs = /\(([\s\S]*?)\)/gi.exec(caller)[1];
var args = arguments.callee.caller.arguments;
var funcBody = caller.replace(/^[\s\S]*?waitFor[\s\S]*?yield;|}[\s;]*$/g,'');
var context = this;
var thread = setInterval(function() {
if(check()) {
clearInterval(thread);
new Function(funcArgs, funcBody).apply(context, args);
}
}, ms?ms:100);
return this;
}
function coroutine1() {
this.a = 100;
console.log('coroutine1-1:start');
return sleepFor(3000).yield;
console.log('coroutine1-2:complete');
this.a++;
}
function coroutine2(c) {
/* Some code here */
this.a = 1;
console.log('coroutine2-1:' + this.a++);
return sleepFor(500).yield;
/* next */
console.log('coroutine2-2:' + this.a++);
console.log('coroutine2-2:waitFor c.a>100:' + c.a);
return waitFor(function() {
return c.a>100;
}).yield;
/* The rest of the code */
console.log('coroutine2-3:' + this.a++);
}
this.a = 10;
console.log('outer-1:' + this.a++);
var c1 = new coroutine1();
var c2 = new coroutine2(c1);
console.log('outer-2:' + this.a++);
Sie wurde in Internet Explorer 11 und Chrome 49 getestet. Weil es verwendet argumente.calllee Daher kann es zu Problemen kommen, wenn es im Strict-Modus läuft.