ソリューションを 1 日ナビゲートしましたが、コールバックを使用して連鎖性を維持する方法をまだ考えています。
コードを 1 行ずつ同期して実行する従来のプログラミング スタイルには、誰もが慣れ親しんでいます。SetTimeout はコールバックを使用するため、次の行は完了するまで待機しません。これにより、「スリープ」機能を作成するために、「同期」する方法を考えることができます。
単純なコルーチンから始めます。
function coroutine() {
console.log('coroutine-1:start');
sleepFor(3000); // Sleep for 3 seconds here
console.log('coroutine-2:complete');
}
途中で 3 秒間スリープさせたいのですが、フロー全体を支配したくないので、コルーチンは別のスレッドで実行する必要があります。Unity YieldInstructionを検討し、コルーチンを次のように変更します。
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();
sleepFor プロトタイプを宣言します。
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;
}
coroutine1 を実行した後 (Internet Explorer 11 と Chrome 49 でテストしました)、2 つのコンソール ステートメントの間に 3 秒間スリープすることがわかります。コードを伝統的なスタイルと同じくらいきれいに保ちます。
注意が必要なのは、sleepFor ルーチンです。呼び出し元の関数本体を文字列として読み取り、2 つの部分に分割します。上部を取り除き、下部で別の機能を作成します。指定されたミリ秒数待機した後、元のコンテキストと引数を適用して、作成された関数を呼び出します。本来の流れの為、通常通り「戻る」で終了となります。「収穫」のため?正規表現のマッチングに使用されます。必要ですが、まったく役に立ちません。
100% 完璧というわけではありませんが、少なくとも私の仕事は達成できます。このコードを使用する際のいくつかの制限について言及する必要があります。コードが 2 つの部分に分割されているため、「return」ステートメントは、ループや {} ではなく、外側にある必要があります。すなわち
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++;
}
上記のコードには、作成された関数に閉じ括弧が個別に存在できないため、問題があるに違いありません。もう 1 つの制限は、「var xxx=123」で宣言されたすべてのローカル変数が次の関数に引き継がれないことです。同じことを達成するには、「this.xxx=123」を使用する必要があります。関数に引数があり、それらが変更された場合、変更された値も次の関数に引き継ぐことができませんでした。
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
}
別の関数プロトタイプを紹介します: 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;
}
true が返されるまで「check」関数を待ちます。100 ミリ秒ごとに値をチェックします。追加の引数を渡すことで調整できます。テスト用の 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++);
}
また、これまでのところ私たちが愛するかわいいスタイルでも。実際、ネストされたコールバックは嫌いです。coroutine2 が coroutine1 の完了を待つことは容易に理解できます。面白い?では、次のコードを実行します。
this.a = 10;
console.log('outer-1:' + this.a++);
var c1 = new coroutine1();
var c2 = new coroutine2(c1);
console.log('outer-2:' + this.a++);
出力は次のとおりです。
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
外側は、coroutine1 と coroutine2 を初期化した直後に完了します。次に、coroutine1 は 3000 ミリ秒待機します。Coroutine2 は、500 ミリ秒待機した後、ステップ 2 に入ります。その後、coroutine1.a の値が 100 を超えることが検出されると、ステップ 3 に進みます。
変数「a」を保持するコンテキストが 3 つあることに注意してください。1 つは外部で、値は 10 と 11 です。もう 1 つは coroutine1 にあり、値は 100 と 101 です。最後のものは coroutine2 にあり、値は 1、2、3 です。coroutine2 では、ca が来るのも待ちます。 coroutine1 から、その値が 100 を超えるまで。3 つのコンテキストは独立しています。
コピー&ペーストのコード全体:
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++);
Internet Explorer 11 と Chrome 49 で動作確認済みです。arguments.callee を使用しているため、 strict モードで実行すると問題が発生する可能性があります。