I navigate the solution for a day but still thinking how to maintain the chainability in using callback. Everyone is familiar with the traditional programming style which running the code line by line in synchronised way. SetTimeout uses a callback so the next line does not wait for it to complete. This let me think how to make it "sync", so as to make a "sleep" function.
Beginning with a simple coroutine:
function coroutine() {
console.log('coroutine-1:start');
sleepFor(3000); //sleep for 3 seconds here
console.log('coroutine-2:complete');
}
I want to sleep 3 seconds in the middle but don't want to dominate the whole flow, so the coroutine must be executed by another thread. I consider the Unity YieldInstruction, and modify the coroutine in the following:
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();
Declare the sleepFor prototype:
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;
}
After run the coroutine1 (I tested in IE11 and Chrome49), you will see it sleep 3 seconds between two console statements. It keeps the codes as pretty as the traditional style. The tricky is in sleepFor routine. It reads the caller function body as string and break it into 2 parts. Remove the upper part and create another function by lower part. After waiting for the specified number of milliseconds, it calls the created function by applying the original context and arguments. For the original flow, it will end by "return" as usual. For the "yield"? It is used for regex matching. It is necessary but no use at all.
It is not 100% perfect at all but it achieves my jobs at least. I have to mention some limitations in using this piece of codes. As the code is being broken into 2 parts, the "return" statement must be in outer, instead of in any loop or {}. i.e.
function coroutine3() {
this.a = 100;
console.log('coroutine3-1:start');
if(true) {
return sleepFor(3000).yield;
} // <- raise exception here
console.log('coroutine3-2:complete');
this.a++;
}
The above codes must have problem as the close bracket could not exist individually in the created function. Another limitation is all local variables declared by "var xxx=123" could not carry to next function. You must use "this.xxx=123" to achieve the same thing. If your function has arguments and they got changes, the modified value also could not carry to next function.
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
}
I would introduce another function prototype: 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;
}
It waits for "check" function until it returns true. It checks the value every 100ms. You can adjust it by passing additional argument. Consider the testing coroutine2:
function coroutine2(c) {
/* some codes 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 code */
console.log('coroutine2-3:' + this.a++);
}
Also in pretty style we love so far. Actually I hate the nested callback. It is easily understood that the coroutine2 will wait for the completion of coroutine1. Interesting? Ok, then run the following codes:
this.a = 10;
console.log('outer-1:' + this.a++);
var c1 = new coroutine1();
var c2 = new coroutine2(c1);
console.log('outer-2:' + this.a++);
The output is:
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 is immediately completed after initialised coroutine1 and coroutine2. Then, coroutine1 will wait for 3000ms. Coroutine2 will enter into step 2 after waited for 500ms. After that, it will continue step 3 once it detects the coroutine1.a values > 100.
Beware of that there are 3 contexts to hold variable "a". One is outer, which values are 10 and 11. Another one is in coroutine1, which values are 100 and 101. The last one is in coroutine2, which values are 1,2 and 3. In coroutine2, it also waits for c.a which comes from coroutine1, until its value is greater than 100. 3 contexts are independent.
The whole code for 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 codes 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 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++);
It is tested in IE11 and Chrome49. Because it uses arguments.callee, so it may be trouble if it runs in strict mode.