I navigated the solutions for a day, but I am still thinking about how to maintain the chainability in using a callback.
Everyone is familiar with the traditional programming style which is running the code line by line in a synchronised way. SetTimeout uses a callback so the next line does not wait for it to complete. This lets 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 I 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 running the coroutine1 (I tested in Internet Explorer 11 and Chrome 49), you will see it sleep 3 seconds between two console statements. It keeps the codes as pretty as the traditional style.
The tricky bit is in sleepFor routine. It reads the caller function body as string and break it into two 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 regular expression 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 two 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 an exception here
console.log('coroutine3-2:complete');
this.a++;
}
The above code must have a 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 the 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 the 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 100 ms. You can adjust it by passing additional argument. Consider the testing 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++);
}
Also in the 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 code:
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 3000 ms. Coroutine2 will enter into step 2 after waited for 500 ms. After that, it will continue step 3 once it detects the coroutine1.a values > 100.
Beware of that there are three 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 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++);
It is tested in Internet Explorer 11 and Chrome 49. Because it uses arguments.callee, so it may be trouble if it runs in strict mode.
sleep()
to time an animation. (I know there are better ways...) The code the questioner presents will not work for me in Chrome, because rather than updating the DOM as soon as the modification is made in script, the browser waits until the code finishes executing before making any DOM updates, so the script waits the sum of all the delays, and then applies all the DOM updates at once. – Candi(s) => new Promise((r) => setTimeout(r, 1000 * s | 0))
– Chouestimport { setTimeout } from 'timers/promises'
andawait setTimeout(2000)
– Dam