为什么这个基于setTimeout的代码在Firefox中不起作用,超时时间很小(在Internet Explorer / Chrome中工作)?



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

预期行为:

  • 当按下第一个按钮时,它显示为已按下,计算运行几秒钟,然后当计算完成时,按钮显示为再次按下,第二列从"尚未计算"变为"计算完成"。(我不会详细说明为什么会发生这种情况;这在相关答案中有解释。)

  • 当按下第二个按钮时,该按钮立即按下;第二列立即变为"正在计算…"文本。几秒钟后计算结束时,第二列从"正在计算…"变为"已完成计算"。

实际发生的情况:

  • 这在Chrome中非常有效(两个按钮的行为都与预期一致)

  • 这在InternetExplorer8中非常有效

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

    • setTimeout()中的超时从0更改为1对没有影响

    • setTimeout()中的超时从0更改为500工作

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

根据setTimeout()工作而没有工作的全部原因,延迟应该对事情的工作方式没有任何影响,因为setTimeout()的主要目的是改变这里的排队顺序,而不是延迟事情。

那么,为什么它在Firefox上不适用于延迟0或1,但在延迟500时却能正常工作(在InternetExplorer8/Chrome上适用于任何延迟)?

更新:除了下面的源代码,我还制作了一个JSFiddle。但由于某种原因,JSFiddle甚至拒绝在我的互联网上加载;资源管理器;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的嵌套循环边界更改为300/100/100;资源管理器;8.或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才能有机会重新绘制屏幕。要做到这一点,您需要等待足够长的时间,这样您的计时器函数就不会立即执行,从而产生收益。

正如你所注意到的,暂停500毫秒就可以了。您可能可以使用较小的数字,例如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=%sn",
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()等。

在使用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以毫秒为单位的最小时间长度,函数可以为其设置超时延迟。默认值为4毫秒(在10毫秒之前)。使用调用setTimeout()小于该值的延迟将被箝位到该最小值。

像0或1这样的值应该表现得像4——不知道这是会导致代码延迟还是会破坏代码。

相关内容

最新更新