鉴于
let doAsynchronousStuff = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve("abcdefg"[Math.floor(Math.random() * 7)])
}, Math.PI * 1 + Math.random())
})
.then(data => console.log(data))
.then(doAsynchronousStuff)
}
该模式是
的实现或;上面没有列出的其他常见模式?
寻找可信和/或官方来源的答案。
答案 0 :(得分:2)
我重写了代码,删除了所有不相关的东西,并且使用了我认为在这种情况下更具可读性和风格的样式。
function doAsynchronousStuff()
{
return new Promise((resolve, reject) =>
{
setTimeout(() => {resolve("test")}, 0)
})
.then(console.log)
.then(doAsynchronousStuff);
}
我们应该分析执行流程,记住JS有an event loop,特别是
setTimeout
发布其参数函数,以便在事件循环的下一个 1 循环中执行。 then
发布其参数函数,以便在事件循环的下一个循环中执行。事件循环的存在很重要,因为函数在重新输入循环之前将消息发送到 run-to-completion 。
还需要很好地了解承诺,例如知道then
会返回新承诺。
执行doAsynchronousStuff
时,构造Promise
对象并立即调用其参数函数。
Execution stack Event loop messages
doAsynchronousStuff
Promise constructor
Closure (resolve, reject)
这反过来调用setTimeout
发布一个事件并返回。
Execution stack Event loop messages
doAsynchronousStuff resolve("test")
Promise constructor
Closure (resolve, reject)
setTimeout
执行回退到doAsynchronousStuff
,它为Promise
对象设置了延续,但当然不执行它们。所以最后doAsynchronousStuff
返回,我们有一个运行到完成的情况。
Execution stack Event loop messages
resolve("test")
事件循环执行resolve("test")
(或更好地包含它的闭包),它将promise设置为已解决,并在下一个周期安排其继续
Execution stack Event loop messages
resolve console.log
resolve
结束我们再次出现 run-to-completion 情况。
Execution stack Event loop messages
console.log
console.log
已执行。实际上,执行了一个调用console.log
的函数,当调用then
时,该函数由promise对象设置。
当console.log
返回其承诺时,doAsynchronousStuff
将在事件循环中发布。
Execution stack Event loop messages
resolve doAsynchronousStuff
当resolve
结束时,我们会有运行到完成并再次执行doAsynchronousStuff
。
现在我不会在数学意义上挖掘太多,也不会在你的问题列表中的CS理论意义上挖掘太多,这样做没有实际的好处,因为我不相信这是一个理论问题。
相反,我会将自己限制在编程的角度。
当第二个doAsynchronousStuff
实例被调用时,第一个实例早已消失( run-to-completion )。
基本上情况相当于这样做
let f = () => { console.log('hi!'); setTimeout(f, 0); }
我不会将此函数称为递归,因为递归意味着将问题破坏为较小的自动相似部件。
递归函数不必直接调用自身,也不必“使堆栈增长”,但必须根据自身定义。
如果它像
let f = () => { f(); }
我称之为(严重)递归。那是什么呢?
我想说一个函数在编程意义上是递归的,如果你没有完成它所做的所有调用就无法完成它。
第一个示例可以在不等待f
的后续调用完成的情况下完成,而第二个示例则不能完成。
在我看来,我称之为f
的第一个版本,已安排。
关于尾调用优化,它与此无关 TCO transform a particular kind of recursion into a loop,它是编译器优化而不是代码的属性 尾部调用是代码的属性,但此代码不是尾部调用,因为它首先不是递归的。
在编程意义上它也是 not iteration (在理论意义上它是),因为 iteration 是通过特定结构(如for
)实现的, while
,goto
)。
这里的边界模糊,因为迭代,递归和调度重叠。
最后,这肯定是一个恰好引用自身的非终止程序的情况。
1 我们在这里做了一个简化,它不是下一个循环,它只是一个未来的循环。
答案 1 :(得分:1)
以上都不是。有问题的代码不是递归的,不是非常迭代的(尽管从英语语言的角度看它是迭代的,从我们通常在编程中调用迭代的观点来看,它不是,请注意,从英语语言递归的角度来看是迭代的,但我们并不是说它在编程中),因为它不是递归的短语" tail-call-optimized"不适用,并且它不是非终止,因为函数以返回结束。
它是一个函数,它调度一系列函数,以便稍后执行其中一个函数。
安排是设计模式。 调度最古老的例子之一是操作系统的进程调度。下一个最古老的例子之一是cron。
调度的工作原理是运行时环境(Linux内核,Windows内核,cron进程,javascript)保存了一个"数据库" (它可以像链接列表一样简单,也可以像SQL一样高级,也可以像文本文件一样低技术)对它应该运行的代码和触发它们的条件进行某种引用(请查看AWS Lambda服务用于这个想法的非常高级的实现)并定期以某种方式检查是否满足条件然后执行代码。
对于OS内核,这组条件包括某种公平算法,以确保所有程序都能使用CPU。对于cron,条件是crontab中的时间规范。对于javascript,条件是回调注册的事件(对于setTimeout,它是超时事件)。
传统上,如果您为此编写自己的软件,则将其编写为简单的状态机。以下是类似C的伪代码,实现与上面示例相同的内容
int tick = 0;
// Assume that there is an API for registering 1ms periodic interrupt
interrupt_1ms periodic () {
tick++;
}
int main (void) {
int timeout = PI + rand(); // a fairly silly way to randomly select 3 or 4 ms
char state = 0;
char result = nul;
char* data = "abcdefg";
while (1) {
if (tick >= timeout && state == 0) {
state = 1;
tick = 0;
timeout = PI + rand();
}
switch (state) {
case 1:
result = data[floor(rand() * 7)];
state = 2;
break;
case 2:
printf("%c", result);
state = 3;
break;
case 3:
state = 0; // reschedule the doAsynchronousStuff
break;
}
}
}
这是传统的方式。 javascript的作用并不完全相同,但在概念上相似。它仍然使用永久循环作为事件循环的核心,但它不会连续运行(这将浪费CPU时间,加热CPU和耗尽电池)。相反,它阻止调用异步I / O API之一(select,poll,epoll,kqueue等 - libuv将在编译时选择)并将控制传递给OS,这将使进程进入睡眠状态,直到其中一个注册的I / O事件被触发。
现在,请注意您的代码:
let doAsynchronousStuff = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve("abcdefg"[Math.floor(Math.random() * 7)])
}, Math.PI * 1 + Math.random())
})
.then(data => console.log(data))
.then(doAsynchronousStuff)
}
我不了解你,但对我来说,理由比传统的状态机容易得多。好的,对于这个非常简单的例子,上面的C伪代码很容易理解,但考虑一个真实的node.js或具有数十或数百个复杂事件的jQuery应用程序(在传统的jQuery应用程序的情况下,这些事件甚至可能不安排自己或安排更多的事件处理程序)。随着你必须处理的事件数增加javascript给你的语法变得更加可读,即使对于一个事件,一个不熟悉匿名函数和异步代码的初学者可能更喜欢我的伪C示例。
即使是老式的非演绎回调也比伪C代码更具可读性:
function doAsynchronousStuff () {
setTimeout(function () {
console.log("abcdefg"[Math.floor(Math.random() * 7)]);
doAsynchronousStuff();
}, Math.PI * 1 + Math.random());
}
所以语法可能是新的(好吧,不是那么新,Lispers在70年代一直在做这类事情)但这个想法已经过时了。由于语法原因,核心概念可能无法识别,因此不会因语法而分心。它只是安排用计时器运行一些东西。我们只需要重复调度"重复安排" (Google日历和Apple日历都会调用它们"重复")。