Pourquoi setTimeout () «s'arrête-t-il» pour des valeurs de délai importantes en millisecondes?

104

Je suis tombé sur un comportement inattendu en passant une valeur importante en millisecondes à setTimeout(). Par exemple,

setTimeout(some_callback, Number.MAX_VALUE);

et

setTimeout(some_callback, Infinity);

les deux causent some_callbackd'être exécutés presque immédiatement, comme si j'avais passé 0au lieu d'un grand nombre comme retard.

Pourquoi cela arrive-t-il?

Matt Ball
la source

Réponses:

143

Cela est dû au fait que setTimeout utilise un int 32 bits pour stocker le délai de sorte que la valeur maximale autorisée soit

2147483647

si tu essayes

2147483648

vous obtenez votre problème.

Je ne peux que supposer que cela provoque une forme d'exception interne dans le moteur JS et que la fonction se déclenche immédiatement plutôt que pas du tout.

Un tir
la source
1
D'accord, cela a du sens. Je suppose que cela ne soulève pas réellement d'exception interne. Au lieu de cela, je le vois soit (1) provoquant un débordement d'entier, soit (2) contraignant en interne le délai à une valeur int 32 bits non signée. Si (1) est le cas, alors je passe vraiment une valeur négative pour le délai. Si c'est (2), quelque chose du genre delay >>> 0se produit, donc le délai passé est nul. Dans tous les cas, le fait que le délai soit stocké sous la forme d'un int non signé 32 bits explique ce comportement. Merci!
Matt Ball
Ancienne mise à jour, mais je viens de trouver que la limite maximale est 49999861776383( 49999861776384provoque le déclenchement instantané du rappel)
maxp
7
@maxp That's because49999861776383 % 2147483648 === 2147483647
David Da Silva Contín
@ DavidDaSilvaContín très tard à cela, mais pouvez-vous expliquer davantage? Vous ne comprenez pas pourquoi 2147483647 n'est pas la limite?
Nick Coad
2
@NickCoad les deux nombres retarderaient la même quantité (c'est-à-dire que 49999861776383 est identique à 2147483647 d'un point de vue 32 bits signé). écrivez-les en binaire, et prenez les 31 derniers bits, ils seront tous 1s.
Mark Fisher
24

Vous pouvez utiliser:

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
la source
2
c'est cool, mais nous perdons la possibilité d'utiliserClearTimeout en raison de la récursivité.
Allan Nienhuis
2
Vous ne perdez pas vraiment la capacité de l'annuler à condition de faire votre comptabilité et de remplacer timeoutId que vous souhaitez annuler dans cette fonction.
charlag
23

Quelques explications ici: http://closure-library.googlecode.com/svn/docs/closure_goog_timer_timer.js.source.html

Les valeurs de délai d'expiration trop élevées pour tenir dans un entier 32 bits signé peuvent entraîner un dépassement de capacité dans FF, Safari et Chrome, entraînant la planification immédiate du délai d'expiration. Il est plus logique de ne pas programmer ces délais, car 24,8 jours est au-delà d'une attente raisonnable pour que le navigateur reste ouvert.

warpech
la source
2
La réponse de warpech a beaucoup de sens - un processus de longue durée comme un serveur Node.JS peut sembler une exception, mais pour être honnête, si vous voulez que quelque chose se produise exactement en 24 et quelques jours avec une précision de la milliseconde alors vous devriez utiliser quelque chose de plus robuste face aux erreurs de serveur et de machine que setTimeout ...
cfogelberg
@cfogelberg, je n'ai pas vu le FF ni aucune autre implémentation du setTimeout(), mais j'espère qu'ils calculent la date et l'heure à laquelle il devrait se réveiller et ne décrémentent pas un compteur sur un tick défini au hasard ... (On peut espérer , au moins)
Alexis Wilke
2
J'exécute Javascript dans NodeJS sur un serveur, 24,8 jours c'est toujours bon, mais je recherche un moyen plus logique de définir un rappel pour qu'il se produise, par exemple, dans 1 mois (30 jours). Quelle serait la voie à suivre pour cela?
Paul
1
J'ai, à coup sûr, eu des fenêtres de navigateur ouvertes plus de 24,8 jours. C'est bizarre pour moi que les navigateurs ne font pas en interne quelque chose comme la solution de Ronen, au moins jusqu'à MAX_SAFE_INTEGER
acjay
1
Qui dit? Je garde mon navigateur ouvert plus de 24 jours ...;)
Pete Alvin
2

Consultez la documentation des nœuds sur les minuteries ici: https://nodejs.org/api/timers.html (en supposant la même chose pour js car c'est un terme tellement omniprésent maintenant basé sur une boucle d'événements

En bref:

Lorsque le délai est supérieur à 2147483647 ou inférieur à 1, le délai sera réglé sur 1.

et le retard est:

Le nombre de millisecondes à attendre avant d'appeler le rappel.

Il semble que votre valeur de délai d'expiration soit réglée par défaut sur une valeur inattendue selon ces règles, peut-être?

SillyGilly
la source
1

Je suis tombé sur cela lorsque j'ai essayé de déconnecter automatiquement un utilisateur avec une session expirée. Ma solution était de simplement réinitialiser le délai d'expiration après un jour et de conserver la fonctionnalité permettant d'utiliser clearTimeout.

Voici un petit exemple de prototype:

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

Usage:

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

Et vous pouvez l'effacer avec la stopTimerméthode:

timer.stopTimer();
Tim
la source
0

Je ne peux pas faire de commentaire mais répondre à tout le monde. Cela prend une valeur non signée (vous ne pouvez pas attendre des millisecondes négatives évidemment) Donc, comme la valeur maximale est "2147483647" lorsque vous entrez une valeur plus élevée, elle commence à partir de 0.

En gros, retard = {VALUE}% 2147483647.

Donc, utiliser un délai de 2147483648 équivaudrait à 1 milliseconde, donc instant proc.

KYGAS
la source
-2
Number.MAX_VALUE

n'est en fait pas un entier. La valeur maximale autorisée pour setTimeout est probablement 2 ^ 31 ou 2 ^ 32. Essayer

parseInt(Number.MAX_VALUE) 

et vous obtenez 1 en retour au lieu de 1.7976931348623157e + 308.

Osmund
la source
13
Ceci est incorrect: Number.MAX_VALUEest un entier. C'est le nombre entier 17976931348623157 suivi de 292 zéros. La raison est parseIntrenvoyée 1parce qu'il convertit d'abord son argument en chaîne, puis recherche la chaîne de gauche à droite. Dès qu'il trouve le .(qui n'est pas un nombre), il s'arrête.
Pauan
1
Au fait, si vous voulez tester si quelque chose est un entier, utilisez la fonction ES6 Number.isInteger(foo). Mais comme il n'est pas encore pris en charge, vous pouvez utiliser à la Math.round(foo) === fooplace.
Pauan
2
@Pauan, en termes d'implémentation, Number.MAX_VALUEn'est pas un entier mais un double. Donc il y a ça ... Un double peut représenter un entier, cependant, car il est utilisé pour sauvegarder des entiers de 32 bits en JavaScript.
Alexis Wilke
1
@AlexisWilke Oui, bien sûr, JavaScript implémente tous les nombres en virgule flottante 64 bits. Si par "entier" vous entendez "binaire 32 bits", ce Number.MAX_VALUEn'est pas un entier. Mais si par "entier" vous entendez le concept mental "d'un entier", alors c'est un entier. En JavaScript, comme tous les nombres sont à virgule flottante 64 bits, il est courant d'utiliser la définition du concept mental d '«entier».
Pauan
Il y a aussi Number.MAX_SAFE_INTEGERmais ce n'est pas le nombre que nous recherchons ici non plus.
tremby