我浏览了一天的解决方案,但我仍在考虑如何在使用回调时保持可链接性。
每个人都熟悉以同步方式逐行运行代码的传统编程风格。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 中测试过)后,您会看到它在两个控制台语句之间休眠 3 秒。它使代码与传统风格一样漂亮。
棘手的一点是在 sleepFor 例程中。它将调用者函数体读取为字符串并将其分成两部分。移除上部并通过下部创建另一个功能。在等待指定的毫秒数后,它通过应用原始上下文和参数来调用创建的函数。对于原始流程,它将像往常一样以“返回”结束。为了“收益”?它用于正则表达式匹配。这是必要的,但根本没有用。
它根本不是 100% 完美,但它至少完成了我的工作。我不得不提到使用这段代码的一些限制。由于代码被分成两部分,“return”语句必须在外部,而不是在任何循环或 {} 中。IE
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++;
}
上面的代码一定有问题,因为在创建的函数中不能单独存在右括号。另一个限制是“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;
}
它等待“检查”功能,直到它返回真。它每 100 毫秒检查一次值。您可以通过传递附加参数来调整它。考虑测试协程2:
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 后立即完成 Outer。然后,coroutine1 将等待 3000 毫秒。Coroutine2 等待 500 毫秒后进入第 2 步。之后,一旦检测到 coroutine1.a 值 > 100,它将继续第 3 步。
请注意,存在三个上下文来保存变量“a”。一个是outer,取值是10和11。另外一个在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,所以如果它在严格模式下运行可能会很麻烦。