Почему этот код на основе setTimeout не работает в Firefox с небольшим таймаутом (работает в Internet Explorer/Chrome)?

У меня есть следующий код, который демонстрирует разницу в вызове долговременной функции непосредственно из триггера события, а не с помощью setTimeout().

Предполагаемое поведение:

  • Когда нажата первая кнопка, она отображается нажатой, расчет выполняется в течение нескольких секунд, а затем, когда вычисление завершается, кнопка снова отображается нажатой, а второй столбец изменяется от "еще не вычисляемого" до "выполненного расчета". (Я не буду подробно останавливаться на том, почему это должно произойти, он объяснил в связанном ответе.)

  • Когда вторая кнопка нажата, кнопка немедленно нажата; второй столбец немедленно изменяется на текст "вычисление...". Когда расчет завершается через несколько секунд, второй столбец изменяется с "вычисления..." на "выполненный расчет".

Что на самом деле происходит:

  • Это отлично работает в Chrome (обе кнопки ведут себя как ожидалось)

  • Это отлично работает в Internet Explorer 8

  • Это НЕ работает в Firefox (v.25) как есть. В частности, вторая кнопка ведет себя на 100% как первая.

    • Изменение таймаута в setTimeout() от 0 до 1 не имеет эффекта

    • Изменение таймаута в setTimeout() от 0 до 500 работает

Что оставляет меня с большой загадкой.

В соответствии со всей причиной, почему setTimeout() работает, а отсутствие одного - нет, задержка должна иметь нулевой эффект на то, как все работает, , поскольку setTimeout() главная цель - изменить порядок очередей здесь, НЕ задерживать вещи.

Итак, почему он не работает с задержкой 0 или 1 в Firefox, но работает как ожидалось с задержкой 500 (и работает с любой задержкой в ​​Internet Explorer 8/Chrome)?

UPDATE: В дополнение к исходному коду ниже я также сделал JSFiddle. Но по какой-то причине JSFiddle отказывается даже загружать мой Internet Explorer 8, поэтому для этого тестирования требуется следующий код.

UPDATE2: Кто-то поднял возможность возникновения проблемы с настройкой конфигурации dom.min_timeout_value в Firefox. Я отредактировал его с 4 до 0, перезапустил браузер, и ничего не было исправлено. Он по-прежнему не работает с таймаутом 0 или 1 и достигает 500.


Вот мой исходный код - я просто сохранил его в файле HTML на диске C: и открылся во всех трех браузерах:

<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>

Для тестирования вам нужно будет изменить границы вложенных циклов на 300/100/100 для Internet Explorer 8; или до 1000/1000/500 для Chrome, из-за различной чувствительности "эта ошибка JS занимает слишком много времени" в сочетании с скоростью двигателя JavaScript.

Ответ 1

В Ubuntu имеется копия текущей (28 июня 2016 г.) реализации window.setTimeout().

Как мы видим, таймер вставляется этой строкой кода:

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

Затем несколько строк ниже имеют оператор if():

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

insertedInfo == mTimeouts.Elements() проверяет, был ли установлен таймер, который был только что вставлен. Следующий блок НЕ выполняет прикрепленную функцию, но основной цикл сразу же замечает, что таймер был отключен, и, таким образом, он будет пропускать состояние IDLE (выход процессора), которое вы ожидаете.

Это ясно (по крайней мере для меня) объясняет поведение, которое вы испытываете. Отрисовка на экране - это другой процесс (задача/поток), и для этого другого процессора необходимо отказаться, чтобы получить возможность повторно рисовать экран. Чтобы это произошло, вам нужно подождать достаточно долго, чтобы ваша функция таймера не выполнялась немедленно, и произошел выход.

Как вы заметили, пауза в 500 мс делает трюк. Вероятно, вы можете использовать меньшее число, например 50 мс. В любом случае это не будет гарантировать, что выход будет иметь место, но вероятность того, что это произойдет, если компьютер, на котором работает этот код, в настоящее время не загружен (т.е. Антивирус в настоящее время не работает на полной скорости в фоновом режиме... )

Полная функция SetTimeout() из Firefox:

(расположение файла в источнике: 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 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() и т.д.

Ответ 2

Я столкнулся с этой проблемой с Firefox при переключении классов CSS с помощью jQuery для управления переходом CSS.

Увеличение продолжительности setTimeout до 50 с 0 помогло, но, поскольку Алексис предположил, что это не было на 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.

Ответ 3

В Firefox минимальное значение для вызовов setTimeout() настраивается и по умолчанию установлено 4 в текущих версиях:

dom.min_timeout_value Минимальный промежуток времени, в миллисекундах, что функция window.setTimeout() может установить задержку таймаута для. Это значение по умолчанию составляет 4 мс (до 10 мс). Вызов функции setTimeout() с помощью меньше, чем это будет зажато до этого минимального значения.

Значения типа 0 или 1 должны вести себя как 4-нет, если это вызовет задержки в коде или просто сломает его.