typescript 如何在嵌套/递归计算时运行事件循环?

fivyi3re  于 2023-06-24  发布在  TypeScript
关注(0)|答案(3)|浏览(167)

使用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中实现这一点,而无需检测调用链上的所有函数?

qmb5sa22

qmb5sa221#

我将添加一个您似乎没有考虑过的替代解决方案:Promise s.或者更具体地说,处理promise的语法sugar:async/await
使用Promise,实现allowEventLoop()函数很简单:

function allowEventLoop () {
    return new Promise((ok,fail) => setTimeout(ok,0));
}

现在,每当你需要挂起当前计算并运行事件循环时,你只需要调用:

await allowEventLoop();

下面是一个使用上述函数的简单递归下降解析器的示例(注意:在Js中的代码,但在Ts中这样做应该是微不足道的):

async function drawTree(node, indent) {
    if (!indent) indent = 0;

    let tree = `${'\t'.repeat(indent)}${node.name}\n`;

    await allowEventLoop();

    if (node.children) {
        for (let child of node.children) {
            tree += await drawTree(child, indent+1);
        }
    }

    return tree;
}

正如您所看到的,递归函数的逻辑几乎没有改变。它看起来与普通同步版本几乎完全相同。主要的区别是你的函数现在返回结果的Promise
使用async/await时,基本上可以跳过调用堆栈。实际上,您实际上是在使用.then()调用链。因此,实际上调用堆栈仍然是1级深度,但您正在动态构建复杂的.then()链。在实践中,它感觉像通常的基于调用堆栈的递归。
要执行的函数队列由Promises不可见地处理--这本质上是一种处理连续传递风格(CPS)代码的设计模式。这类似于调用堆栈管理要返回的函数队列的方式。这就是为什么感觉是一样的。

r7s23pms

r7s23pms2#

我们希望在长时间运行的、相互递归的函数调用期间启用事件处理。(例如,递归树搜索)在一定深度或时间之后,搜索希望自愿暂停执行,以允许顶层事件循环运行(处理鼠标/按键事件,重绘图形等)
理想的情况是一个系统级函数runEventLoop(),它“产生”当前计算,将自己的延续放在事件队列中,并将控制权交给系统EventLoop。
JavaScript似乎只提供了部分解决方案:

  • 'setTimeout()'将在事件队列上放置一个函数[但不是当前的延续]
  • 'yield'将挂起当前延续,但不会将其放在事件队列中。而'yield'返回一个值给Generator的调用者调用堆栈上一级。因此,调用者必须已经具有Generator形式的“continuation”。

我们还注意到,虽然未捕获的“抛出”会将控制权返回到顶层,但JS中没有办法(TIKO)恢复并重新启动“抛出”的计算。(从顶层通过相互递归调用到自愿的“收益率”)
所以:为了从自愿的收益返回控制,向上通过嵌套或互递归函数,一直到系统EventLoop,我们做3件事:
1.每个函数[caller & called]必须声明为function*(这样它就可以产生)
1.每个函数[caller]必须测试其[caller]的后代是否挂起,如果挂起了,则yield本身以将'yield'传播到顶层:

let result, genR = calledStarFunction(args);
       while (result = genR.next(), !result.done) yield;
       use (result.value)

注意:#2不能有效地 Package 在函数中...因为该函数将服从#1,而 that 函数的调用者服从#2

1.在顶层,使用setTimeout(() => genR.next())返回到JS EventLoop,然后重新启动挂起的函数链。
[之前#2是显而易见的,我写了这个typescript代码,现在'yieldR'是内联的,如上所示]

/** <yield: void, return: TReturn, yield-in: unknown> */
    export type YieldR<TReturn> = Generator<void, TReturn, unknown>
    /**
     * Top-level function to give control to JS Event Loop, and then restart the stack of suspended functions.
     * 'genR' will restart the first/outermost suspended block, which will have code like *yieldR()
     * that loops to retry/restart the next/inner suspended function.
     * @param genR 
     * @param done 
     */
    export function allowEventLoop<T>(genR: YieldR<T>, done?: (result: T) => void): void  {
      let result = genR.next()
      if (result.done) done && done(result.value)
      else setTimeout(() => allowEventLoop(genR, done))
    }
    /** 
     * Return next result from genR. 
     * If genR returns an actual value, return that value
     * If genR yields<void> then propagate a 'yield' to each yieldR up to allowEventLoop(); 
     * 
     * This shows the canonical form of the code.
     * It's not useful to actually *call* this code since it also returns a Generator,
     * and the calling code must then write a while loop to handle the yield-vs-return!
     */
    export function* yieldR<T extends object> (genR: YieldR<T>, log?:string) {
      let result: IteratorResult<void, T>
      while (result = genR.next(), !result.done) yield
      return result.value
    }

**注意:*大多数记录的function 的用法是创建一个Iterator,在这种情况下,'yield'提供有趣/有用的值,并在完成时发出'return'信号。在此用例中,其被反转:yield给出了一个信号,但没有感兴趣的值,“return”提供了感兴趣的计算值。

  • 向JS诸神求助:* 提供一个函数:runEventLoop()透明地将当前的continuation(整个堆栈)放在事件循环上,并将控制直接返回到顶层。因此所有其它呼叫者和调用栈不需要知道在较低级别进行的挂起/恢复。
    **后注:**看起来像这样使用生成器会对性能产生重大影响。内联代码将嵌套的Generator从4个减少到2个之后,代码运行速度提高了10倍。因此,CPS或数据流设计可能适用于复杂/时间敏感的应用程序。(但是,它仍然在开发/调试期间工作,以使KBD/图形运行)
    **另一个注意事项:**Chrome强制最小'setTimeout'延迟为4 ms;因此,如果你计算1 ms,然后屈服4 ms,这是缓慢的,可以解释上面的注解。它有助于计算从上次yield到Date.now()的增量,并且仅当该增量大于[20 -- 200 ms?](取决于您需要的响应程度)。
w7t8yxp5

w7t8yxp53#

为了具体化替代方法(数据流/函数队列),请考虑以下内容:为了保持调用堆栈短,将应用程序划分为多个任务(不递归返回的函数)。如果你要进行递归调用,那么用途:callLater(()=>recursiveTask(arg1, arg2, ...))并返回。callLater将闭包[data and continuation]放在queue上,顶层可以依次处理它。
因此,对于树搜索,在层N,您将任务入队以处理层N+1的节点,加上一个任务来收集和合并结果,然后返回。排队的最后一个任务应该返回最终结果。这个“最终”任务可能包括如下内容:if (queue.length > 0) callLater(finalTask),因此它将自己放在队列的末尾,直到所有其他子任务都已计算完毕并停止向队列添加任务。[或者您可能使用一些Promises并使用Promise.all(...)触发finalTask]
下面的代码还在循环中包含一个计时器,以便运行多个任务,直到超过阈值(并返回到JavaScript事件循环)

type FUNC<T> = ()=>T
const callQueue: Array<FUNC<any>> = []
function callLater(fun: FUNC<any>) {
  callQueue.push(fun)
}
function topLevel<T>(start: FUNC<T>, done?: (value: T) => void, threshold = 30, ms0 = Date.now()) {
  var dms: number
  while ((dms = Date.now() - ms0) < threshold) {
    let value = start()    // which may invoke callLater() to enqueue more tasks
    if (callQueue.length == 0) return done && done(value)
  }
  setTimeout(() => topLevel(callQueue.shift(), done, threshold))
}

相关问题