繁体   English   中英

为什么基于setTimeout的代码不能在Firefox中以很小的超时时间运行(在Internet Explorer / Chrome中运行)?

[英]Why doesn't this setTimeout-based code work in Firefox with a small timeout (works in Internet Explorer/Chrome)?

我有以下代码,演示了直接从事件触发器调用长时间运行的函数与使用setTimeout()的区别。

预期行为:

  • 当第一个按钮被按下时,它似乎被按下,计算运行几秒钟,然后,当计算完成时,该按钮再次出现按下状态,第二列从“尚未计算”变为“计算完成”。 (我不会详细说明为什么会发生这种情况; 在链接的答案中对此进行了解释 。)

  • 当按下第二个按钮时,该按钮立即按下; 第二列立即变为“计算中...”文本。 几秒钟后计算完成时,第二列从“计算中...”变为“计算完成”。

实际发生的情况:

  • 这在Chrome中完美运行(两个按钮的行为均符合预期)

  • 这在Internet Explorer 8中完美运行

  • 按原样在Firefox(v.25)中不起作用。 具体来说,第二个按钮的行为与第一个按钮相同,为100%。

    • setTimeout()的超时从0更改为1无效

    • setTimeout()的超时从0更改为500 有效

这给我留下了很大的难题。

根据setTimeout()工作而不是缺少setTimeout()的全部原因,延迟应该对事物的工作方式产生零影响, 因为setTimeout()的主要目的是更改此处的排队顺序,而不是延迟事物

那么,为什么它不能在Firefox上的延迟0或1下工作,而在延迟500下却可以正常工作(并且在Internet Explorer 8 / Chrome上可以工作在任何延迟下)?

更新:除了下面的源代码,我还制作了一个JSFiddle 但是出于某种原因,JSFiddle甚至拒绝在我的Internet Explorer 8上加载,因此对于该测试,需要以下代码。

UPDATE2:有人提出了Firefox中的配置设置dom.min_timeout_value存在问题的可能性。 我已将其从4修改为0,重新启动了浏览器,但未修复任何问题。 它仍然失败,超时为0或1,成功为500。


这是我的源代码-我只是将其保存到C:驱动器上的HTML文件中,并在所有三种浏览器中打开:

<html><body>
<script src="http://code.jquery.com/jquery-1.9.1.js"></script>

<table border=1>
    <tr><td><button id='do'>Do long calc - bad status!</button></td>
        <td><div id='status'>Not Calculating yet.</div></td></tr>
    <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
        <td><div id='status_ok'>Not Calculating yet.</div></td></tr>
</table>

<script>
function long_running(status_div) {
    var result = 0;
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 200; k++) {
                result = result + i + j + k;
            }
        }
    }
    $(status_div).text('calclation done');
}

// Assign events to buttons
$('#do').on('click', function () {
    $('#status').text('calculating....');
    long_running('#status');
});
$('#do_ok').on('click', function () {
    $('#status_ok').text('calculating....');
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
});
</script>
</body></html>

要进行测试,您需要将Internet Explorer 8的嵌套循环边界更改为300/100/100;否则,请执行以下步骤。 或将Chrome设置为1000/1000/500,这是由于“此JS花费的时间太长”错误的敏感性不同以及JavaScript引擎速度所致。

在Ubuntu中有window.setTimeout()的当前(2016年6月28日)实现的副本。

如我们所见,计时器通过以下代码行插入:

  nsAutoPtr<TimeoutInfo>* insertedInfo =
    mTimeouts.InsertElementSorted(newInfo.forget(), GetAutoPtrComparator(mTimeouts));

然后,在下面的行中有一个if()语句:

if (insertedInfo == mTimeouts.Elements() && !mRunningExpiredTimeouts) {
...

insertedInfo == mTimeouts.Elements()检查刚刚插入的计时器是否已经超时。 以下块不执行附加功能,但是主循环将立即注意到计时器超时,因此它将跳过您期望的IDLE状态(CPU的产量)。

这(至少对我而言)清楚地解释了您所遇到的行为。 屏幕上的渲染是另一个进程(任务/线程),需要放弃CPU来执行该其他进程,以便有机会重新绘制屏幕。 为此,您需要等待足够长的时间,以使您的计时器函数不会立即执行并产生收益。

正如您所注意到的,暂停500ms可以解决问题。 您可以使用较小的数字,例如50ms。 无论哪种方式都不能保证产生收益,但是如果运行该代码的计算机当前未被淹没(即,防病毒软件当前未在后台全速运行),则很有可能会发生。 )

Firefox的完整SetTimeout()函数:

(文件在源中的位置: dom/workers/WorkerPrivate.cpp

int32_t
WorkerPrivate::SetTimeout(JSContext* aCx,
                          dom::Function* aHandler,
                          const nsAString& aStringHandler,
                          int32_t aTimeout,
                          const Sequence<JS::Value>& aArguments,
                          bool aIsInterval,
                          ErrorResult& aRv)
{
  AssertIsOnWorkerThread();

  const int32_t timerId = mNextTimeoutId++;

  Status currentStatus;
  {
    MutexAutoLock lock(mMutex);
    currentStatus = mStatus;
  }

  // It's a script bug if setTimeout/setInterval are called from a close handler
  // so throw an exception.
  if (currentStatus == Closing) {
    JS_ReportError(aCx, "Cannot schedule timeouts from the close handler!");
  }

  // If the worker is trying to call setTimeout/setInterval and the parent
  // thread has initiated the close process then just silently fail.
  if (currentStatus >= Closing) {
    aRv.Throw(NS_ERROR_FAILURE);
    return 0;
  }

  nsAutoPtr<TimeoutInfo> newInfo(new TimeoutInfo());
  newInfo->mIsInterval = aIsInterval;
  newInfo->mId = timerId;

  if (MOZ_UNLIKELY(timerId == INT32_MAX)) {
    NS_WARNING("Timeout ids overflowed!");
    mNextTimeoutId = 1;
  }

  // Take care of the main argument.
  if (aHandler) {
    newInfo->mTimeoutCallable = JS::ObjectValue(*aHandler->Callable());
  }
  else if (!aStringHandler.IsEmpty()) {
    newInfo->mTimeoutString = aStringHandler;
  }
  else {
    JS_ReportError(aCx, "Useless %s call (missing quotes around argument?)",
                   aIsInterval ? "setInterval" : "setTimeout");
    return 0;
  }

  // See if any of the optional arguments were passed.
  aTimeout = std::max(0, aTimeout);
  newInfo->mInterval = TimeDuration::FromMilliseconds(aTimeout);

  uint32_t argc = aArguments.Length();
  if (argc && !newInfo->mTimeoutCallable.isUndefined()) {
    nsTArray<JS::Heap<JS::Value>> extraArgVals(argc);
    for (uint32_t index = 0; index < argc; index++) {
      extraArgVals.AppendElement(aArguments[index]);
    }
    newInfo->mExtraArgVals.SwapElements(extraArgVals);
  }

  newInfo->mTargetTime = TimeStamp::Now() + newInfo->mInterval;

  if (!newInfo->mTimeoutString.IsEmpty()) {
    if (!nsJSUtils::GetCallingLocation(aCx, newInfo->mFilename, &newInfo->mLineNumber)) {
      NS_WARNING("Failed to get calling location!");
    }
  }

  nsAutoPtr<TimeoutInfo>* insertedInfo =
    mTimeouts.InsertElementSorted(newInfo.forget(), GetAutoPtrComparator(mTimeouts));

  LOG(TimeoutsLog(), ("Worker %p has new timeout: delay=%d interval=%s\n",
                      this, aTimeout, aIsInterval ? "yes" : "no"));

  // If the timeout we just made is set to fire next then we need to update the
  // timer, unless we're currently running timeouts.
  if (insertedInfo == mTimeouts.Elements() && !mRunningExpiredTimeouts) {
    nsresult rv;

    if (!mTimer) {
      mTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
      if (NS_FAILED(rv)) {
        aRv.Throw(rv);
        return 0;
      }

      mTimerRunnable = new TimerRunnable(this);
    }

    if (!mTimerRunning) {
      if (!ModifyBusyCountFromWorker(true)) {
        aRv.Throw(NS_ERROR_FAILURE);
        return 0;
      }
      mTimerRunning = true;
    }

    if (!RescheduleTimeoutTimer(aCx)) {
      aRv.Throw(NS_ERROR_FAILURE);
      return 0;
    }
  }

  return timerId;
}

重要说明: JavaScript指令yield ,与我所说的无关。 我正在谈论sched_yield()功能,该功能在二进制进程调用某些函数(例如sched_yield()本身, poll()select()select()时发生。

我在使用jQuery切换CSS类以控制CSS过渡时遇到了Firefox的问题。

将setTimeout的持续时间从0增加到50有帮助,但是正如Alexis所建议的那样,这并不是100%可靠的。

我发现最好的解决方案是将间隔计时器与IF语句结合使用,以在触发转换之前实际检查是否已应用必要的样式,而不是使用setTimeout并假定执行已按预期的顺序进行,例如

    var firefox_pause = setInterval(function() {
            //Test whether page is ready for next step - in this case the div must have a max height applied
        if ($('div').css('max-height') != "none") {
            clear_firefox_pause();
            //Add next step in queue here
        }
    }, 10);

    function clear_firefox_pause() {
        clearInterval(firefox_pause);
    }

至少就我而言,这似乎在Firefox中每次都有效。

在Firefox中,setTimeout()调用的最小值是可配置的 ,在当前版本中默认为4:

dom.min_timeout_value window.setTimeout()函数可以为其设置超时延迟的最小时间长度(以毫秒为单位)。 默认为4毫秒(10毫秒之前)。 延迟小于此时间的setTimeout()调用将被限制为该最小值。

诸如0或1之类的值应表现为4 —不知道这是否会导致代码延迟或只是破坏代码。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM