¿Por qué setTimeout () se “rompe” para valores de retardo de milisegundos grandes?

104

Me encontré con un comportamiento inesperado al pasar un gran valor de milisegundos a setTimeout(). Por ejemplo,

setTimeout(some_callback, Number.MAX_VALUE);

y

setTimeout(some_callback, Infinity);

ambos causan some_callbackque se ejecuten casi de inmediato, como si hubiera pasado en 0lugar de un gran número como retraso.

¿Por qué pasó esto?

Matt Ball
fuente

Respuestas:

143

Esto se debe a que setTimeout usa un int de 32 bits para almacenar el retraso, por lo que el valor máximo permitido sería

2147483647

si intentas

2147483648

usted consigue que su problema ocurra.

Solo puedo suponer que esto está causando algún tipo de excepción interna en JS Engine y haciendo que la función se active inmediatamente en lugar de no dispararse en absoluto.

Un trago
fuente
1
Vale, eso tiene sentido. Supongo que en realidad no genera una excepción interna. En cambio, lo veo (1) causando un desbordamiento de enteros o (2) coaccionando internamente el retraso a un valor int de 32 bits sin signo. Si (1) es el caso, entonces realmente estoy pasando un valor negativo para el retraso. Si es (2), entonces delay >>> 0sucede algo como , por lo que el retraso pasado es cero. De cualquier manera, el hecho de que el retraso se almacene como un int sin firmar de 32 bits explica este comportamiento. ¡Gracias!
Matt Ball
Actualización anterior, pero acabo de descubrir que el límite máximo es 49999861776383( 49999861776384hace que la devolución de llamada se
active
7
@maxp Eso es porque49999861776383 % 2147483648 === 2147483647
David Da Silva Contín
@ DavidDaSilvaContín muy tarde para esto, pero ¿puedes explicarme más? ¿No entiendes por qué 2147483647 no es el límite?
Nick Coad
2
@NickCoad ambos números retrasarían la misma cantidad (es decir, 49999861776383 es ​​igual que 2147483647 desde un punto de vista de 32 bits con signo). escríbalos en binario y tome los últimos 31 bits, todos serán unos.
Mark Fisher
24

Puedes usar:

function runAtDate(date, func) {
    var now = (new Date()).getTime();
    var then = date.getTime();
    var diff = Math.max((then - now), 0);
    if (diff > 0x7FFFFFFF) //setTimeout limit is MAX_INT32=(2^31-1)
        setTimeout(function() {runAtDate(date, func);}, 0x7FFFFFFF);
    else
        setTimeout(func, diff);
}
Ronen
fuente
2
esto es genial, pero perdemos la capacidad de usar ClearTimeout debido a la recursividad.
Allan Nienhuis
2
Realmente no pierde la capacidad de cancelarlo siempre que haga su contabilidad y reemplace el timeoutId que desea cancelar dentro de esta función.
charlag
23

Alguna explicación aquí: http://closure-library.googlecode.com/svn/docs/closure_goog_timer_timer.js.source.html

Los valores de tiempo de espera demasiado grandes para caber en un entero de 32 bits con signo pueden causar un desbordamiento en FF, Safari y Chrome, lo que hace que el tiempo de espera se programe inmediatamente. Tiene más sentido simplemente no programar estos tiempos de espera, ya que 24,8 días es más que una expectativa razonable para que el navegador permanezca abierto.

warpech
fuente
2
La respuesta de warpech tiene mucho sentido: un proceso de larga ejecución como un servidor Node.JS puede parecer una excepción, pero para ser honesto, si tiene algo que desea asegurarse de que suceda exactamente en 24 y unos días con una precisión de milisegundos entonces debería usar algo más robusto frente a los errores del servidor y de la máquina que setTimeout ...
cfogelberg
@cfogelberg, no he visto el FF ni ninguna otra implementación del setTimeout(), pero espero que calculen la fecha y la hora en que debería despertarse y no disminuyan un contador en algún tick definido al azar ... (Uno puede esperar , al menos)
Alexis Wilke
2
Estoy ejecutando Javascript en NodeJS en un servidor, 24,8 días sigue siendo bueno, pero estoy buscando una forma más lógica de configurar una devolución de llamada en, por ejemplo, 1 mes (30 días). ¿Cuál sería el camino a seguir para esto?
Paul
1
Seguro que he tenido las ventanas del navegador abiertas durante más de 24,8 días. Es extraño para mí que los navegadores no hagan internamente algo como la solución de Ronen, al menos hasta MAX_SAFE_INTEGER
acjay
1
¿Quien dice? Mantengo mi navegador abierto más de 24 días ...;)
Pete Alvin
2

Consulte el documento del nodo sobre Timers aquí: https://nodejs.org/api/timers.html (asumiendo lo mismo en js también, ya que ahora es un término tan ubicuo en el bucle de eventos basado

En breve:

Cuando la demora es mayor que 2147483647 o menor que 1, la demora se establecerá en 1.

y el retraso es:

La cantidad de milisegundos que se deben esperar antes de llamar a la devolución de llamada.

¿Parece que su valor de tiempo de espera está predeterminado en un valor inesperado según estas reglas, posiblemente?

SillyGilly
fuente
1

Me encontré con esto cuando intenté cerrar la sesión automáticamente de un usuario con una sesión caducada. Mi solución fue restablecer el tiempo de espera después de un día y mantener la funcionalidad para usar clearTimeout.

Aquí hay un pequeño ejemplo de prototipo:

Timer = function(execTime, callback) {
    if(!(execTime instanceof Date)) {
        execTime = new Date(execTime);
    }

    this.execTime = execTime;
    this.callback = callback;

    this.init();
};

Timer.prototype = {

    callback: null,
    execTime: null,

    _timeout : null,

    /**
     * Initialize and start timer
     */
    init : function() {
        this.checkTimer();
    },

    /**
     * Get the time of the callback execution should happen
     */
    getExecTime : function() {
        return this.execTime;
    },

    /**
     * Checks the current time with the execute time and executes callback accordingly
     */
    checkTimer : function() {
        clearTimeout(this._timeout);

        var now = new Date();
        var ms = this.getExecTime().getTime() - now.getTime();

        /**
         * Check if timer has expired
         */
        if(ms <= 0) {
            this.callback(this);

            return false;
        }

        /**
         * Check if ms is more than one day, then revered to one day
         */
        var max = (86400 * 1000);
        if(ms > max) {
            ms = max;
        }

        /**
         * Otherwise set timeout
         */
        this._timeout = setTimeout(function(self) {
            self.checkTimer();
        }, ms, this);
    },

    /**
     * Stops the timeout
     */
    stopTimer : function() {
        clearTimeout(this._timeout);
    }
};

Uso:

var timer = new Timer('2018-08-17 14:05:00', function() {
    document.location.reload();
});

Y puedes borrarlo con el stopTimermétodo:

timer.stopTimer();
Tim
fuente
0

No puedo comentar, pero responder a todas las personas. Se necesita un valor sin firmar (obviamente, no puede esperar milisegundos negativos). Entonces, dado que el valor máximo es "2147483647", cuando ingresa un valor más alto, comienza a ir desde 0.

Básicamente, retraso = {VALOR}% 2147483647.

Entonces, usar un retraso de 2147483648 lo haría 1 milisegundo, por lo tanto, proceso instantáneo.

KYGAS
fuente
-2
Number.MAX_VALUE

en realidad no es un número entero. El valor máximo permitido para setTimeout es probablemente 2 ^ 31 o 2 ^ 32. Tratar

parseInt(Number.MAX_VALUE) 

y obtienes 1 de vuelta en lugar de 1.7976931348623157e + 308.

Osmund
fuente
13
Esto es incorrecto: Number.MAX_VALUEes un número entero. Es el número entero 17976931348623157 con 292 ceros después. La razón por la que parseIntregresa 1es porque primero convierte su argumento en una cadena y luego busca la cadena de izquierda a derecha. Tan pronto como encuentra el .(que no es un número), se detiene.
Pauan
1
Por cierto, si quieres probar si algo es un número entero, usa la función ES6 Number.isInteger(foo). Pero como aún no es compatible, puede usar Math.round(foo) === fooen su lugar.
Pauan
2
@Pauan, en cuanto a la implementación, Number.MAX_VALUEno es un número entero sino un double. Entonces hay que ... Un doble puede representar un número entero, ya que se usa para guardar números enteros de 32 bits en JavaScript.
Alexis Wilke
1
@AlexisWilke Sí, por supuesto JavaScript implementa todos los números como punto flotante de 64 bits. Si por "entero" te refieres a "binario de 32 bits", entonces Number.MAX_VALUEno es un número entero. Pero si por "entero" te refieres al concepto mental de "un número entero", entonces es un número entero. En JavaScript, debido a que todos los números son de coma flotante de 64 bits, es común usar la definición de concepto mental de "entero".
Pauan
También está, Number.MAX_SAFE_INTEGERpero ese no es el número que estamos buscando aquí.
temblor