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

后端 未结 3 1282
时光取名叫无心
时光取名叫无心 2020-12-09 12:45

I have the following code which demonstrates the difference in calling a long-running function directly from an event trigger, vs. using setTimeout().

<

相关标签:
3条回答
  • 2020-12-09 13:18

    In Firefox, the minimum value for setTimeout() calls is configurable and defaults to 4 in current versions:

    dom.min_timeout_value The minimum length of time, in milliseconds, that the window.setTimeout() function can set a timeout delay for. This defaults to 4 ms (before 10 ms). Calls to setTimeout() with a delay smaller than this will be clamped to this minimum value.

    Values like 0 or 1 should behave like 4—no idea if that will cause delays in your code or just break it.

    0 讨论(0)
  • 2020-12-09 13:22

    I faced this issue with Firefox while toggling CSS classes using jQuery to control a CSS transition.

    Increasing the duration of setTimeout to 50 from 0 helped, but as Alexis suggested this wasn’t 100% reliable.

    The best (if longwinded) solution I found was to combine an interval timer with an IF statement to actually check whether the necessary styles had been applied before triggering the transition, rather using setTimeout and assuming execution had taken place in the intended order, e.g.

        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);
        }
    

    In my case at least, this seems to work every time in Firefox.

    0 讨论(0)
  • 2020-12-09 13:34

    There is a copy of the current (Jun 28, 2016) implementation of window.setTimeout() in Ubuntu.

    As we can see, the timer gets inserted by this line of code:

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

    Then a few lines below you have an if() statement:

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

    The insertedInfo == mTimeouts.Elements() checks whether the timer that was just inserted already timed out. The following block does NOT execute the attached function, but the main loop will immediately notice that a timer timed out and thus it will skip the IDLE state (a yield of the CPU) that you are expecting.

    This clearly (at least to me) explains the behavior you are experiencing. The rendering on the screen is another process (task/thread) and the CPU needs to be relinquished for that other process to get a chance to re-paint the screen. For that to happen, you need to wait long enough so your timer function does not get executed immediately and a yield happens.

    As you've notice a pause of 500ms does the trick. You can probably use a smaller number, such as 50ms. Either way it is not going to guarantee that a yield happens, but chances are it will happen if the computer on which that code is running is not currently swamped (i.e. an anti-virus is not currently running full speed in the background...)

    The complete SetTimeout() function from Firefox:

    (location of the file in the source: 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;
    }
    

    IMPORTANT NOTE: The JavaScript instruction yield, has nothing to do with what I am talking about. I am talking about the sched_yield() functionality which happens when a binary process calls certain functions, such as sched_yield() itself, poll(), select(), etc.

    0 讨论(0)
提交回复
热议问题