【问题标题】:too much recursion when wrapping setTimeout() in web worker在 web worker 中包装 setTimeout() 时递归过多
【发布时间】:2020-03-31 21:33:38
【问题描述】:

我正在尝试将 self.setTimeout() 函数包装在网络工作者中(因此没有本机窗口对象)。我认为使用方法 apply 语法会非常简单:

window.setTimeout = function ( /**/) {
    return setTimeout.apply(self,arguments); 
};

这个调用应该等同于使用任意数量的参数(函数、延迟、传递给函数的参数)调用 self.setTimeout() 并返回超时的 id。

但是,中间的代码行最终会抛出“递归过多”的错误。看起来像这样调用它以某种方式破坏了此处描述的机制:Will a recursive 'setTimeout' function call eventually kill the JS Engine? 破坏了先前的函数上下文并使其实际上是递归的。以防它是特定于浏览器的:在 Firefox 74.0(64 位)中测试。

一些背景

以防万一有人想知道,我为什么要这样做:

我想在不重写所有内容的情况下将一些 CPU 密集型代码从主线程转移到 web-workers。

不幸的是,代码依赖于一些库,而这些库又依赖于存在的窗口和文档,否则它不会初始化。

作为一种解决方法,我在工作人员内部使用Mock Dom。因为我实际上并不想进行 DOM 操作,所以我只是以最简单的方式添加模拟 dom 中缺少的任何功能。但出于某种原因,库有时会显式调用 window.setTimeout() 而不仅仅是 setTimeout() - 所以我需要将此函数添加到模拟窗口对象中,并且它需要正常工作。

更新

谢谢,Alexandre Senges 指出了这个函数实际上会调用自己的错误。

在 web-Worker 中实际可行的解决方案是

window.setTimeout = function ( /**/) {
    return DedicatedWorkerGlobalScope.prototype.setTimeout.apply(self,arguments); 
};

所以,我需要编写函数的整个路径,以防止 window.setTimeout 调用自身。

更新 2

实际上,正如 Kaiido 的另一个回答所指出的那样,我最初的想法本身应该可以正常工作。发生过多递归的原因是我在有效复制self.setTimeout = window.setTimeout的代码的其他部分出错 - 从而导致setTimeout===self.setTimeout===window.setTimeout 因此函数window.setTimeout突然无意识地递归。

【问题讨论】:

    标签: javascript web-worker


    【解决方案1】:

    当您一遍又一遍地调用同一个函数本身太多次时,就会出现最大递归深度。原因是每次调用函数时,都必须为其所有堆栈变量创建新实例。

    例如:

    const recursive = (x) => {
        console.log(x);
        recursive(x+1);
    }
    

    这里,每次函数调用自己时,它都会创建一个新的 x 并将其推送到堆栈中,而不会弹出之前的 x,因为我们还没有从调用者那里返回。如果我们让这种情况发生太久,堆栈就会变满,我们会得到一个StackOverflow(因此是网站的名称。)

    现在,JS 通过限制调用堆栈的深度来保护我们不让这种情况发生。

    在您的示例中,问题是由于您无意中创建了递归函数。当您调用新的window.setTimeout 时,它会调用setTimeout.apply,它指代新函数本身,因此它会再次调用自身等。一种解决方法是将先前的方法提取到新变量中,然后再调用它:

    window.oldTimeout = window.setTimeout;
    window.setTimeout = function ( /**/) {
        return oldTimeout.apply(self,arguments); 
    };
    

    但是,我认为您正在尝试做的事情完全是一个坏主意。您不应该尝试替换 window 方法,因为它会为您的所有程序甚至可能依赖于它们的库更改它。如果我是你,我会简单地创建一个新函数myTimeout

    【讨论】:

    • 在我创建它之前不存在 window.setTimeout。正如我在第一句话中所说,我在一个网络工作者内部,而网络工作者没有窗口对象。所有全局变量都是 DedicatedWorkerGlobalScope 类型对象的一部分。所以如果我简单地写 setTimeout(),这实际上是 DedicatedWorkerGlobalScope 的 setTimeout 而不是 window.setTimeout()
    • @wulf21 我对它调用自己这一事实没有任何影响。您将不得不更改为 window.setTimeout = () => DedicatedWorkerGlobalScope.setTimeout()...
    • @wulf21 我对WebWorkers不熟悉,但问题是一样的,你在自己内部调用相同的函数
    • 您好,感谢您的提示。找到解决方案就足够了。我会将这个答案标记为正确,并用我实际解决它的方式更新我的帖子。
    【解决方案2】:

    您的代码应该可以正常工作:

    const worker_script = `
      const window = {};
      window.setTimeout = function ( /**/) {
        return setTimeout.apply(self,arguments); 
      };
      window.setTimeout( (val)=>postMessage('done ' + val), 200, 'ok');
    `;
    const blob = new Blob( [ worker_script ] );
    const url = URL.createObjectURL( blob );
    const worker = new Worker( url );
    worker.onmessage = e => console.log( e.data );
    worker.onerror = console.error;

    为了得到这个错误,您可能确实将window 设置为self

    const worker_script = `
      const window = self;
      window.setTimeout = function ( /**/) {
        return setTimeout.apply(self,arguments); 
      };
      window.setTimeout( (val)=>postMessage('done ' + val), 200, 'ok');
    `;
    const blob = new Blob( [ worker_script ] );
    const url = URL.createObjectURL( blob );
    const worker = new Worker( url );
    worker.onmessage = e => console.log( e.data );
    worker.onerror = console.error;

    如果是这样,你甚至不需要重写任何东西:

    const worker_script = `
      const window = self;
      window.setTimeout( (val)=>postMessage('done ' + val), 200, 'ok');
    `;
    const blob = new Blob( [ worker_script ] );
    const url = URL.createObjectURL( blob );
    const worker = new Worker( url );
    worker.onmessage = e => console.log( e.data );
    worker.onerror = console.error;

    【讨论】:

    • 你好。是的,这个答案实际上更有意义。我无法理解为什么我的原始代码会导致无休止的递归循环,只是说这可能是由于 JS 的解释性质并接受了它。但要澄清一下:我没有将window 设置为self,但 window 实际上是一个模拟 dom 窗口:var window = $DOM( {docReadyState:"complete" } ); 但很可能代码中还有其他部分 - 无论是我自己的还是库 - 这将出于某种原因设置 self.setTimeout = window.setTimeout。
    • 您的答案确实是完全正确的(考虑到我的代码的其他部分可能会做的事情缺少知识)。我犯了一个没有引发语法错误的小错误。这个错误导致代码位有效地将所有成员从window复制到self——包括setTimeout——而不是将库X注入window的每个成员复制到自己,什么是意图,
    猜你喜欢
    • 2015-08-07
    • 1970-01-01
    • 2011-04-07
    • 1970-01-01
    • 1970-01-01
    • 2020-04-16
    • 1970-01-01
    • 1970-01-01
    • 2017-11-05
    相关资源
    最近更新 更多