javascript - 为什么这个基于 setTimeout 的代码不能在 Firefox 中以小超时工作(在 Internet Explorer/Chrome 中工作)?

标签 javascript html firefox cross-browser settimeout

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

预期行为:

  • 当按下第一个按钮时,它显示为按下,计算运行几秒钟,然后当计算完成时,按钮再次显示为按下,第二列从“尚未计算”变为“计算完成” ”。 (我不会详细说明为什么会发生这种情况;it's explained in linked answer。)

  • 按下第二个按钮时,按钮立即按下;第二列立即变为“正在计算...”文本。数秒后计算完成时,第二列从“正在计算...”变为“计算完成”。

实际发生了什么:

  • 这在 Chrome 中完美运行(两个按钮都按预期运行)

  • 这在 Internet Explorer 8 中非常有效

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

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

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

这给我留下了一个大难题。

根据 setTimeout() 有效而缺少一个无效的全部原因,延迟对工作方式的影响应该为零,因为 setTimeout() 的主要目的是改变这里的排队顺序,而不是拖延事情

那么,为什么它不能在 Firefox 上使用延迟 0 或 1,但可以按预期使用延迟 500(并且可以在 Internet Explorer 8/Chrome 上使用任何延迟)?

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

更新 2:有人提出 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 中有一份当前(2016 年 6 月 28 日)window.setTimeout() 实现的副本。

正如我们所见,定时器是通过这行代码插入的:

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

然后几行下面有一个if()语句:

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

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

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

正如您所注意到的,暂停 500 毫秒就可以了。您或许可以使用较小的数字,例如 50 毫秒。无论哪种方式,它都不能保证产生 yield ,但如果运行该代码的计算机当前没有被淹没(即反病毒当前没有在后台全速运行...... )

来自 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()

关于javascript - 为什么这个基于 setTimeout 的代码不能在 Firefox 中以小超时工作(在 Internet Explorer/Chrome 中工作)?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20747591/

相关文章:

javascript - Javascript 中字符串的长度始终为 1?

javascript - 如何从 Node.js Express 服务器将 JSON 数据返回到 Riot.js?

javascript - jsrender (jsviews) 绑定(bind)处理程序

html - div 具有三个子 div 的内部 css 问题

html - 身份验证重定向 - 在 Firefox 扩展的新选项卡中打开本地 HTML 文档

javascript - 如何将 <g> 元素添加到我的图表

javascript - 如何在javascript中设置按钮值

html - 调整窗口大小时在 CSS 中从顶部锚定图像

jquery - 如何让 "jQuery.Event"在 Firefox 上工作?

css - 剪辑路径中 SVG 路径的动画在 Firefox 中不起作用?