Найти время, оставшееся в setTimeout()?

Я пишу некоторый Javascript, который взаимодействует с библиотечным кодом, который мне не принадлежит, и не может (разумно) измениться. Он создает тайм-ауты Javascript, используемые для показа следующего вопроса в серии ограниченных по времени вопросов. Это не настоящий код, потому что он запутан вне всякой надежды. Вот что делает библиотека:

....
// setup a timeout to go to the next question based on user-supplied time
var t = questionTime * 1000
test.currentTimeout = setTimeout( showNextQuestion(questions[i+1]), t );

Я хочу поставить индикатор на экране, который заполняет questionTime * 1000 опросив таймер, созданный setTimeout, Единственная проблема в том, что, похоже, нет способа сделать это. Есть ли getTimeout функция, которую я пропускаю? Единственная информация о тайм-аутах Javascript, которую я могу найти, связана только с созданием через setTimeout( function, time) и удаление через clearTimeout( id ),

Я ищу функцию, которая возвращает либо время, оставшееся до срабатывания тайм-аута, либо время, прошедшее после вызова тайм-аута. Мой индикатор состояния выглядит так:

var  timeleft = getTimeout( test.currentTimeout ); // I don't know how to do this
var  $bar = $('.control .bar');
while ( timeleft > 1 ) {
    $bar.width(timeleft / test.defaultQuestionTime * 1000);
}

tl; dr: Как мне найти время, оставшееся до javascript setTimeout()?


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

// setup a timeout to go to the next question based on user-supplied time
var t = questionTime * 1000
test.currentTimeout = mySetTimeout( showNextQuestion(questions[i+1]), t );

и вот мой код:

// обертка для setTimeout
function mySetTimeout( func, timeout) {
    timeouts[ n = setTimeout( func, timeout) ] = {
        начало: новая дата ().getTime(),
        end: new Date().getTime() + timeout
        t: тайм-аут
    }
    вернуть n;
}

Это работает довольно точно в любом браузере, кроме IE 6. Даже в оригинальном iPhone, где я ожидал, что все станет асинхронным.

16 ответов

Решение

Если вы не можете изменить код библиотеки, вам нужно переопределить setTimeout в соответствии с вашими целями. Вот пример того, что вы могли бы сделать:

(function () {
var nativeSetTimeout = window.setTimeout;

window.bindTimeout = function (listener, interval) {
    function setTimeout(code, delay) {
        var elapsed = 0,
            h;

        h = window.setInterval(function () {
                elapsed += interval;
                if (elapsed < delay) {
                    listener(delay - elapsed);
                } else {
                    window.clearInterval(h);
                }
            }, interval);
        return nativeSetTimeout(code, delay);
    }

    window.setTimeout = setTimeout;
    setTimeout._native = nativeSetTimeout;
};
}());
window.bindTimeout(function (t) {console.log(t + "ms remaining");}, 100);
window.setTimeout(function () {console.log("All done.");}, 1000);

Это не рабочий код, но он должен поставить вас на правильный путь. Обратите внимание, что вы можете привязать только одного слушателя за время ожидания. Я не проводил обширных испытаний с этим, но он работает в Firebug.

Более надежное решение будет использовать ту же технику обертывания setTimeout, но вместо этого использовать карту из возвращенного timeoutId для слушателей для обработки нескольких слушателей за один тайм-аут. Вы могли бы также рассмотреть упаковку clearTimeout, чтобы вы могли отсоединить слушателя, если время ожидания сброшено.

Просто для записи, есть способ получить время, оставшееся в node.js:

var timeout = setTimeout(function() {}, 3600 * 1000);

setInterval(function() {
    console.log('Time left: '+getTimeLeft(timeout)+'s');
}, 2000);

function getTimeLeft(timeout) {
    return Math.ceil((timeout._idleStart + timeout._idleTimeout - Date.now()) / 1000);
}

Печать:

$ node test.js 
Time left: 3599s
Time left: 3597s
Time left: 3595s
Time left: 3593s

Похоже, это не работает в Firefox, но поскольку node.js - это javascript, я подумал, что это замечание может быть полезно для людей, которые ищут решение для узла.

РЕДАКТИРОВАТЬ: я на самом деле думаю, что я сделал еще лучше: /questions/43024530/najti-vremya-ostavsheesya-v-settimeout/43024550#43024550

Я написал эту функцию, и я часто ее использую:

function timer(callback, delay) {
    var id, started, remaining = delay, running

    this.start = function() {
        running = true
        started = new Date()
        id = setTimeout(callback, remaining)
    }

    this.pause = function() {
        running = false
        clearTimeout(id)
        remaining -= new Date() - started
    }

    this.getTimeLeft = function() {
        if (running) {
            this.pause()
            this.start()
        }

        return remaining
    }

    this.getStateRunning = function() {
        return running
    }

    this.start()
}

Сделать таймер:

a = new timer(function() {
    // What ever
}, 3000)

Так что если вы хотите, чтобы оставшееся время просто делалось

a.getTimeLeft()

Специфичный Node.js на стороне сервера

Ничто из вышеперечисленного не помогло мне, и после проверки объекта тайм-аута все выглядело так, как будто все было относительно начала процесса. Следующее работало для меня:

myTimer = setTimeout(function a(){console.log('Timer executed')},15000);

function getTimeLeft(timeout){
  console.log(Math.ceil((timeout._idleStart + timeout._idleTimeout)/1000 - process.uptime()));
}

setInterval(getTimeLeft,1000,myTimer);

Выход:

14
...
3
2
1
Timer executed
-0
-1
...

node -v
v9.11.1

Для краткости отредактировал вывод, но эта базовая функция дает приблизительное время до выполнения или после выполнения. Как упоминают другие, ничего из этого не будет точным из-за того, как узлы обрабатывают, но если я хочу подавить запрос, который был запущен менее 1 минуты назад, и я сохранил таймер, я не вижу, почему это не будет работать как быстрая проверка. Может быть интересно жонглировать объектами с освежителем в 10.2+.

Более быстрый и простой способ:

tmo = 1000;
start = performance.now();
setTimeout(function(){
    foo();
},tmo);

Вы можете узнать оставшееся время с помощью:

 timeLeft = tmo - (performance.now() - start);

Стеки событий Javascript не работают так, как вы думаете.

Когда событие тайм-аута создается, оно добавляется в очередь событий, но другие события могут иметь приоритет во время запуска этого события, задерживая время выполнения и откладывая время выполнения.

Пример: вы создаете тайм-аут с задержкой 10 секунд, чтобы что-то предупредить на экране. Он будет добавлен в стек событий и будет выполнен после запуска всех текущих событий (что вызывает некоторую задержку). Затем, когда время ожидания обработано, браузер все еще продолжает захватывать другие события, добавляя их в стек, что вызывает дальнейшие задержки в обработке. Если пользователь щелкает или выполняет много нажатий клавиш Ctrl+, его события имеют приоритет над текущим стеком. Ваши 10 секунд могут превратиться в 15 секунд или дольше.


При этом есть много способов подделать, сколько времени прошло. Одним из способов является выполнение setInterval сразу после добавления setTimeout в стек.

Пример. Выполните заданное время ожидания с задержкой в ​​10 секунд (сохраните эту задержку в глобальном формате). Затем выполните setInterval, который выполняется каждую секунду, чтобы вычесть 1 из задержки и вывести оставшуюся задержку. Из-за того, как стек событий может влиять на фактическое время (описано выше), это все еще не будет точным, но дает счет.


Короче говоря, нет реального способа получить оставшееся время. Есть только способы попытаться передать оценку пользователю.

Вот, возможно, еще лучший способ сделать это, плюс вам не нужно будет менять код, который вы уже написали:

var getTimeout = (function() { // IIFE
    var _setTimeout = setTimeout, // Reference to the original setTimeout
        map = {}; // Map of all timeouts with their start date and delay

    setTimeout = function(callback, delay) { // Modify setTimeout
        var id = _setTimeout(callback, delay); // Run the original, and store the id

        map[id] = [Date.now(), delay]; // Store the start date and delay

        return id; // Return the id
    };

    return function(id) { // The actual getTimeLeft function
        var m = map[id]; // Find the timeout in map

        // If there was no timeout with that id, return NaN, otherwise, return the time left clamped to 0
        return m ? Math.max(m[1] - Date.now() + m[0], 0) : NaN;
    }
})();

... и подражали

var getTimeout=function(){var e=setTimeout,b={};setTimeout=function(a,c){var d=e(a,c);b[d]=[Date.now(),c];return d};return function(a){return(a=b[a])?Math.max(a[1]-Date.now()+a[0],0):NaN}}();

Вы можете изменить setTimeout сохранить время окончания каждого таймаута на карте и создать функцию под названием getTimeout чтобы получить время для тайм-аута с определенным идентификатором.

Это было super решение, но я изменил его, чтобы использовать немного меньше памяти

let getTimeout = (() => { // IIFE
    let _setTimeout = setTimeout, // Reference to the original setTimeout
        map = {}; // Map of all timeouts with their end times

    setTimeout = (callback, delay) => { // Modify setTimeout
        let id = _setTimeout(callback, delay); // Run the original, and store the id
        map[id] = Date.now() + delay; // Store the end time
        return id; // Return the id
    };

    return (id) => { // The actual getTimeout function
        // If there was no timeout with that id, return NaN, otherwise, return the time left clamped to 0
        return map[id] ? Math.max(map[id] - Date.now(), 0) : NaN;
    }
})();

Использование:

// go home in 4 seconds
let redirectTimeout = setTimeout(() => {
    window.location.href = "/index.html";
}, 4000);

// display the time left until the redirect
setInterval(() => {
    document.querySelector("#countdown").innerHTML = `Time left until redirect ${getTimeout(redirectTimeout)}`;
},1);

Вот уменьшенная версия этого getTimeout IIFE:

let getTimeout=(()=>{let t=setTimeout,e={};return setTimeout=((a,o)=>{let u=t(a,o);return e[u]=Date.now()+o,u}),t=>e[t]?Math.max(e[t]-Date.now(),0):NaN})();

Я надеюсь, что это так же полезно для вас, как и для меня!:)

Я зашел сюда в поисках этого ответа, но задумался над моей проблемой. Если вы находитесь здесь, потому что вам просто нужно следить за временем, пока вы используете setTimeout, вот еще один способ сделать это:

    var focusTime = parseInt(msg.time) * 1000

    setTimeout(function() {
        alert('Nice Job Heres 5 Schrute bucks')
        clearInterval(timerInterval)
    }, focusTime)

    var timerInterval = setInterval(function(){
        focusTime -= 1000
        initTimer(focusTime / 1000)
    }, 1000);

Я создал класс, позволяющий вам приостанавливать и возобновлять тайм-ауты и интервалы, надеюсь, это поможет!

      class CustomTimer {
  constructor() {
    this._initState();
  }

  start(callback, delay, isInterval = false) {
    if (
      typeof callback !== "function" ||
      typeof delay !== "number" ||
      delay <= 0
    ) {
      throw new Error("Invalid arguments provided to start method.");
    }
    this.stop(); // Clear any previous timer

    this._userCallback = callback;
    this._startTime = Date.now();
    this._endTime = this._startTime + delay;
    this._remaining = delay;

    if (isInterval) {
      this._interval = delay;
    }

    this._startTimer(delay);
  }

  pause() {
    if (!this._timerId || this._isPaused) return;

    this._isPaused = true;
    this._remaining -= Date.now() - this._startTime;

    this._clearTimer();
  }

  resume() {
    if (!this._isPaused) return;

    this._startTimer(this._remaining);

    this._isPaused = false;
    this._startTime = Date.now();
    this._endTime = this._startTime + this._remaining;
  }

  stop() {
    this._clearTimer();
    this._initState();
  }

  _initState() {
    this._startTime = null;
    this._endTime = null;
    this._remaining = null;
    this._interval = null;
    this._userCallback = null;
    this._timerId = null;
    this._isPaused = false;
  }

  _startTimer(delay) {
    // If it's an interval and it's paused, then on resume, we just use a setTimeout.
    if (this._interval && this._isPaused) {
      this._timerId = setTimeout(() => {
        this._tick();
        this._timerId = setInterval(this._tick, this._interval);
      }, delay);
    } else {
      const timerFn = this._interval ? setInterval : setTimeout;
      this._timerId = timerFn(this._tick, delay);
    }
  }

  _clearTimer() {
    if (this._timerId) {
      // clearInterval works for both interval and timeout
      clearInterval(this._timerId);
      this._timerId = null;
    }
  }

  // Using an arrow function here ensures 'this' retains the current instance's context
  // We could also use .bind(this) in the constructor
  // We need to do this because setInterval and setTimeout change the context of 'this'
  _tick = () => {
    if (this._isPaused) return;

    this._userCallback();

    if (this._interval) {
      this._remaining = this._interval;
      this._startTime = Date.now();
      this._endTime = this._startTime + this._remaining;
    } else {
      this.stop();
    }
  };

  get timerId() {
    return this._timerId;
  }

  get timeRemaining() {
    if (this._isPaused) return this._remaining;
    return Math.max(this._endTime - Date.now(), 0);
  }
}

// Example usage

const timer = new CustomTimer();

timer.start(
  () => {
    console.log("Hello!");
  },
  5000,
  true
); // Execute the callback every 5 seconds

setTimeout(() => {
  timer.pause();
  console.log("Timer paused");
}, 2000); // Pause the timer after 2 seconds

setTimeout(() => {
  timer.resume(); // Resume the timer after 4 seconds (2 seconds after it was paused)
}, 4000);

setInterval(() => {
  console.log(`Time remaining: ${timer.timeRemaining}ms.`); // Get the time remaining every second
}, 1000);

Проверьте это:

class Timer {
  constructor(fun,delay) {
    this.timer=setTimeout(fun, delay)
    this.stamp=new Date()
  }
  get(){return ((this.timer._idleTimeout - (new Date-this.stamp))/1000) }
  clear(){return (this.stamp=null, clearTimeout(this.timer))}
}

Сделать таймер:

let smtg = new Timer(()=>{do()}, 3000})

Получить остаются:

smth.get()

Очистить тайм-аут

smth.clear()

На вопрос уже ответили, но я добавлю свой бит. Это только что произошло со мной.

использование setTimeout в recursion следующее:

var count = -1;

function beginTimer()
{
    console.log("Counting 20 seconds");
    count++;

    if(count <20)
    {
        console.log(20-count+"seconds left");
        setTimeout(beginTimer,2000);
    }
    else
    {
        endTimer();
    }
}

function endTimer()
{
    console.log("Time is finished");
}

Я думаю, что код говорит сам за себя

Если кто-то оглядывается на это. Я выступил с менеджером тайм-аутов и интервалов, который может дать вам время, оставшееся в тайм-ауте или интервале, а также сделать некоторые другие вещи. Я добавлю к нему, чтобы сделать его более изящным и точным, но, похоже, он работает довольно хорошо (хотя у меня есть еще несколько идей, чтобы сделать его еще более точным):

https://github.com/vhmth/Tock

    (function(){
        window.activeCountdowns = [];
        window.setCountdown = function (code, delay, callback, interval) {
            var timeout = delay;
            var timeoutId = setTimeout(function(){
                clearCountdown(timeoutId);
                return code();
            }, delay);
            window.activeCountdowns.push(timeoutId);
            setTimeout(function countdown(){
                var key = window.activeCountdowns.indexOf(timeoutId);
                if (key < 0) return;
                timeout -= interval;
                setTimeout(countdown, interval);
                return callback(timeout);
            }, interval);
            return timeoutId;
        };
        window.clearCountdown = function (timeoutId) {
            clearTimeout(timeoutId);
            var key = window.activeCountdowns.indexOf(timeoutId);
            if (key < 0) return;
            window.activeCountdowns.splice(key, 1);
        };
    })();

    //example
    var t = setCountdown(function () {
        console.log('done');
    }, 15000, function (i) {
        console.log(i / 1000);
    }, 1000);

Нет, но у вас может быть свой собственный setTimeout/setInterval для анимации в вашей функции.

Скажите, что ваш вопрос выглядит так:

function myQuestion() {
  // animate the progress bar for 1 sec
  animate( "progressbar", 1000 );

  // do the question stuff
  // ...
}

И ваша анимация будет обрабатываться этими двумя функциями:

function interpolate( start, end, pos ) {
  return start + ( pos * (end - start) );
}

function animate( dom, interval, delay ) {

      interval = interval || 1000;
      delay    = delay    || 10;

  var start    = Number(new Date());

  if ( typeof dom === "string" ) {
    dom = document.getElementById( dom );
  }

  function step() {

    var now     = Number(new Date()),
        elapsed = now - start,
        pos     = elapsed / interval,
        value   = ~~interpolate( 0, 500, pos ); // 0-500px (progress bar)

    dom.style.width = value + "px";

    if ( elapsed < interval )
      setTimeout( step, delay );
  }

  setTimeout( step, delay );
}

Для тех, кто нуждается в крючке, проверьте это - должно быть довольно понятно.

Обратите внимание, что elapsed — это внутренняя переменная состояния, которая, если ее передать за пределы хука, будет неверной!

      import { useEffect, useRef, useState } from 'react';

const useTimeout = (callback, duration, renderDuration = 5) => {
  const ref = useRef<any>(null);
  const [timeInfo, setTimeInfo] = useState<{
    start: number;
    elapsed: number;
    percentComplete: number;
  }>({
    start: null,
    elapsed: 0,
    percentComplete: 0
  });

  useEffect(() => {
    return () => {
      if (ref.current) {
        clearTimeout(ref.current);
        ref.current = null;
      }
    };
  }, []);

  useEffect(() => {
    setTimeout(() => {
      if (ref.current == null) return;
      setTimeInfo((prev) => {
        const elapsed = Date.now() - prev.start + prev.elapsed;

        if (ref.current == null) return prev;
        return {
          start: prev.start,
          elapsed: prev.elapsed,
          percentComplete: (elapsed / duration) * 100
        };
      });
    }, renderDuration);
  }, [timeInfo]);

  return {
    percentComplete: timeInfo.percentComplete,
    isTimerRunning: ref.current != null,
    startTimeout: () => {
      if (ref.current != null) return;
      setTimeInfo((prev) => ({ ...prev, start: Date.now() }));
      ref.current = setTimeout(callback, duration - timeInfo.elapsed);
    },
    stopTimeout: () => {
      if (ref.current) {
        clearTimeout(ref.current);
        ref.current = null;
      }
      setTimeInfo((prev) => {
        const elapsed = Date.now() - prev.start + prev.elapsed;
        return {
          start: prev.start,
          elapsed: elapsed,
          percentComplete: (elapsed / duration) * 100
        };
      });
    },
    resetTimeout: () => {
      if (ref.current) {
        ref.current = null;
        clearTimeout(ref.current);
      }
      setTimeInfo({ start: null, elapsed: 0, percentComplete: 0 });
    },
    restartTimeout: () => {
      if (ref.current) {
        ref.current = null;
        clearTimeout(ref.current);
      }
      setTimeInfo({ start: Date.now(), elapsed: 0, percentComplete: 0 });
      ref.current = setTimeout(callback, duration);
    }
  };
};

export default useTimeout;
Другие вопросы по тегам