使用setTimeout()
破坏计算和释放的常见示例似乎依赖于具有浅(1-deep)调用堆栈。但是,当你在做一个深度嵌套或相互递归的计算(比如树搜索),并且你在堆栈中有很多上下文的时候呢?
如果JavaScript有一个函数可以封装“当前continuation”(即:当前调用堆栈),将其放到事件队列中,然后返回/抛出/回调到顶级事件循环。(因此其他事件将运行,然后计算将从停止的地方重新开始)。我正在寻找一种简单的方法,让一个函数“自愿”“屈服”控制,让事件赶上,然后将控制返回到我们离开的地方。最好不重写调用链中的每个函数。
但我找不到任何能做到这一点的东西...
- 作为一个退休的策划者,我期待着像call/cc这样的东西,但没有找到它。
setTimeout()
将返回控制[但仅向上1级],并重新启动一些 * 其他 * 计算(但不是隐式的current-continuation,除非我们将整个应用程序提交给CPS...)- 'yield'将框化当前函数/堆栈帧的继续,因此它可以重新启动,但yield仅返回上一级。(yield就像:return/cc vs call/cc)
- 'throw'可以向上抛出堆栈,但没有工具从抛出点重新开始计算(据我所知;需要类似'throw/cc'的内容)
我已经使用'yield'构建了一个半解决方案,但它很笨拙,要求堆栈上的每个函数(a)声明为'function*',(b)在每个调用周围包含样板代码,直到下一个函数[传播yield并使用next()重新启动]
问:有没有一种方法可以在JavaScript中实现这一点,而无需检测调用链上的所有函数?
3条答案
按热度按时间qmb5sa221#
我将添加一个您似乎没有考虑过的替代解决方案:
Promise
s.或者更具体地说,处理promise的语法sugar:async/await
。使用
Promise
,实现allowEventLoop()
函数很简单:现在,每当你需要挂起当前计算并运行事件循环时,你只需要调用:
下面是一个使用上述函数的简单递归下降解析器的示例(注意:在Js中的代码,但在Ts中这样做应该是微不足道的):
正如您所看到的,递归函数的逻辑几乎没有改变。它看起来与普通同步版本几乎完全相同。主要的区别是你的函数现在返回结果的
Promise
。使用
async/await
时,基本上可以跳过调用堆栈。实际上,您实际上是在使用.then()
调用链。因此,实际上调用堆栈仍然是1级深度,但您正在动态构建复杂的.then()
链。在实践中,它感觉像通常的基于调用堆栈的递归。要执行的函数队列由Promises不可见地处理--这本质上是一种处理连续传递风格(CPS)代码的设计模式。这类似于调用堆栈管理要返回的函数队列的方式。这就是为什么感觉是一样的。
r7s23pms2#
我们希望在长时间运行的、相互递归的函数调用期间启用事件处理。(例如,递归树搜索)在一定深度或时间之后,搜索希望自愿暂停执行,以允许顶层事件循环运行(处理鼠标/按键事件,重绘图形等)
理想的情况是一个系统级函数runEventLoop(),它“产生”当前计算,将自己的延续放在事件队列中,并将控制权交给系统EventLoop。
JavaScript似乎只提供了部分解决方案:
我们还注意到,虽然未捕获的“抛出”会将控制权返回到顶层,但JS中没有办法(TIKO)恢复并重新启动“抛出”的计算。(从顶层通过相互递归调用到自愿的“收益率”)
所以:为了从自愿的收益返回控制,向上通过嵌套或互递归函数,一直到系统EventLoop,我们做3件事:
1.每个函数[caller & called]必须声明为function*(这样它就可以产生)
1.每个函数[caller]必须测试其[caller]的后代是否挂起,如果挂起了,则yield本身以将'yield'传播到顶层:
注意:#2不能有效地 Package 在函数中...因为该函数将服从#1,而 that 函数的调用者服从#2
1.在顶层,使用
setTimeout(() => genR.next())
返回到JS EventLoop,然后重新启动挂起的函数链。[之前#2是显而易见的,我写了这个typescript代码,现在'yieldR'是内联的,如上所示]
**注意:*大多数记录的function 的用法是创建一个Iterator,在这种情况下,'yield'提供有趣/有用的值,并在完成时发出'return'信号。在此用例中,其被反转:yield给出了一个信号,但没有感兴趣的值,“return”提供了感兴趣的计算值。
**后注:**看起来像这样使用生成器会对性能产生重大影响。内联代码将嵌套的Generator从4个减少到2个之后,代码运行速度提高了10倍。因此,CPS或数据流设计可能适用于复杂/时间敏感的应用程序。(但是,它仍然在开发/调试期间工作,以使KBD/图形运行)
**另一个注意事项:**Chrome强制最小'setTimeout'延迟为4 ms;因此,如果你计算1 ms,然后屈服4 ms,这是缓慢的,可以解释上面的注解。它有助于计算从上次yield到Date.now()的增量,并且仅当该增量大于[20 -- 200 ms?](取决于您需要的响应程度)。
w7t8yxp53#
为了具体化替代方法(数据流/函数队列),请考虑以下内容:为了保持调用堆栈短,将应用程序划分为多个任务(不递归返回的函数)。如果你要进行递归调用,那么用途:
callLater(()=>recursiveTask(arg1, arg2, ...))
并返回。callLater
将闭包[data and continuation]放在queue
上,顶层可以依次处理它。因此,对于树搜索,在层N,您将任务入队以处理层N+1的节点,加上一个任务来收集和合并结果,然后返回。排队的最后一个任务应该返回最终结果。这个“最终”任务可能包括如下内容:
if (queue.length > 0) callLater(finalTask)
,因此它将自己放在队列的末尾,直到所有其他子任务都已计算完毕并停止向队列添加任务。[或者您可能使用一些Promises并使用Promise.all(...)
触发finalTask
]下面的代码还在循环中包含一个计时器,以便运行多个任务,直到超过阈值(并返回到JavaScript事件循环)