Warum "bricht" setTimeout () für große Millisekunden-Verzögerungswerte?

104

Ich bin auf ein unerwartetes Verhalten gestoßen, als ich einen großen Millisekundenwert an übergeben habe setTimeout(). Zum Beispiel,

setTimeout(some_callback, Number.MAX_VALUE);

und

setTimeout(some_callback, Infinity);

beide führen some_callbackdazu, dass sie fast sofort ausgeführt werden, als hätte ich 0anstelle einer großen Zahl die Verzögerung überschritten .

Warum passiert das?

Matt Ball
quelle

Antworten:

143

Dies liegt daran, dass setTimeout ein 32-Bit-int verwendet, um die Verzögerung so zu speichern, dass der maximal zulässige Wert wäre

2147483647

wenn du es versuchst

2147483648

Sie bekommen Ihr Problem auftreten.

Ich kann nur vermuten, dass dies eine interne Ausnahme in der JS Engine verursacht und dazu führt, dass die Funktion sofort und überhaupt nicht ausgelöst wird.

OneSHOT
quelle
1
Okay, das macht Sinn. Ich vermute, es wird keine interne Ausnahme ausgelöst. Stattdessen sehe ich entweder (1) einen ganzzahligen Überlauf verursachen oder (2) die Verzögerung intern auf einen vorzeichenlosen 32-Bit-Int-Wert zwingen. Wenn (1) der Fall ist, übergebe ich wirklich einen negativen Wert für die Verzögerung. Wenn es (2) ist, delay >>> 0passiert so etwas wie , so dass die verstrichene Verzögerung Null ist. In jedem Fall erklärt die Tatsache, dass die Verzögerung als 32-Bit-Int ohne Vorzeichen gespeichert ist, dieses Verhalten. Vielen Dank!
Matt Ball
Altes Update, aber ich habe gerade festgestellt, dass das maximale Limit ist 49999861776383( 49999861776384bewirkt, dass der Rückruf sofort
ausgelöst wird
7
@maxp Das liegt daran49999861776383 % 2147483648 === 2147483647
David Da Silva Contín
@ DavidDaSilvaContín wirklich spät dazu, aber kannst du es weiter erklären? Sie können nicht verstehen, warum 2147483647 nicht das Limit ist?
Nick Coad
2
@NickCoad beide Nummern würden den gleichen Betrag verzögern (dh 49999861776383 ist aus signierter 32-Bit-Sicht identisch mit 2147483647). Schreiben Sie sie binär aus und nehmen Sie die letzten 31 Bits. Sie sind alle 1s.
Mark Fisher
24

Sie können verwenden:

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
quelle
2
Das ist cool, aber wir verlieren aufgrund der Rekursion die Möglichkeit, ClearTimeout zu verwenden.
Allan Nienhuis
2
Sie verlieren nicht wirklich die Fähigkeit, es abzubrechen, vorausgesetzt, Sie führen Ihre Buchhaltung durch und ersetzen die Zeitüberschreitungs-ID, die Sie innerhalb dieser Funktion abbrechen möchten.
Charlag
23

Einige Erklärungen hier: http://closure-library.googlecode.com/svn/docs/closure_goog_timer_timer.js.source.html

Timeout-Werte, die zu groß sind, um in eine vorzeichenbehaftete 32-Bit-Ganzzahl zu passen, können zu einem Überlauf in FF, Safari und Chrome führen, sodass das Timeout sofort geplant wird. Es ist sinnvoller, diese Zeitüberschreitungen einfach nicht zu planen, da 24,8 Tage keine vernünftige Erwartung darstellen, dass der Browser geöffnet bleibt.

Warpech
quelle
2
Die Antwort von warpech ist sehr sinnvoll - ein lang laufender Prozess wie ein Node.JS-Server mag wie eine Ausnahme klingen, aber um ehrlich zu sein, wenn Sie etwas haben, das Sie sicherstellen möchten, geschieht es in genau 24 und ein paar Tagen mit Millisekundengenauigkeit dann sollten Sie etwas Robusteres gegen Server- und Maschinenfehler verwenden als setTimeout ...
cfogelberg
@cfogelberg, ich habe das FF oder eine andere Implementierung des nicht gesehen setTimeout(), aber ich würde hoffen, dass sie das Datum und die Uhrzeit berechnen, zu der es aufwachen sollte, und keinen Zähler auf einem zufällig definierten Tick dekrementieren ... (Man kann hoffen , zumindest)
Alexis Wilke
2
Ich führe Javascript in NodeJS auf einem Server aus. 24,8 Tage sind immer noch gut, aber ich suche nach einer logischeren Möglichkeit, einen Rückruf so einzustellen, dass er beispielsweise in einem Monat (30 Tagen) erfolgt. Was wäre der Weg dafür?
Paul
1
Ich habe sicher Browserfenster länger als 24,8 Tage geöffnet. Es ist bizarr für mich, dass Browser intern nicht so etwas wie Ronens Lösung tun, zumindest nicht bis zu MAX_SAFE_INTEGER
Uhr
1
Wer sagt? Ich halte meinen Browser länger als 24 Tage geöffnet ...;)
Pete Alvin
2

Schauen Sie sich das Knotendokument zu Timern hier an: https://nodejs.org/api/timers.html (vorausgesetzt, dass dies auch für js gilt, da es sich derzeit um einen so allgegenwärtigen Begriff handelt, der auf Ereignisschleifen basiert

Zusamenfassend:

Wenn die Verzögerung größer als 2147483647 oder kleiner als 1 ist, wird die Verzögerung auf 1 gesetzt.

und Verzögerung ist:

Die Anzahl der Millisekunden, die gewartet werden muss, bevor der Rückruf aufgerufen wird.

Scheint, als würde Ihr Timeout-Wert nach diesen Regeln standardmäßig auf einen unerwarteten Wert gesetzt, möglicherweise?

SillyGilly
quelle
1

Ich bin darauf gestoßen, als ich versucht habe, einen Benutzer mit einer abgelaufenen Sitzung automatisch abzumelden. Meine Lösung bestand darin, das Timeout nach einem Tag zurückzusetzen und die Funktionalität für die Verwendung von clearTimeout beizubehalten.

Hier ist ein kleines Prototypbeispiel:

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

Verwendung:

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

Und Sie können es mit der stopTimerMethode löschen :

timer.stopTimer();
Tim
quelle
0

Ich kann nicht kommentieren, aber um allen Leuten zu antworten. Es nimmt einen vorzeichenlosen Wert an (Sie können offensichtlich keine negativen Millisekunden abwarten). Da der maximale Wert also "2147483647" ist, wenn Sie einen höheren Wert eingeben, beginnt er bei 0.

Grundsätzlich Verzögerung = {VALUE}% 2147483647.

Die Verwendung einer Verzögerung von 2147483648 würde also 1 Millisekunde ergeben, daher sofortiger Prozess.

KYGAS
quelle
-2
Number.MAX_VALUE

ist eigentlich keine ganze Zahl. Der maximal zulässige Wert für setTimeout beträgt wahrscheinlich 2 ^ 31 oder 2 ^ 32. Versuchen

parseInt(Number.MAX_VALUE) 

und Sie erhalten 1 zurück statt 1.7976931348623157e + 308.

Osmund
quelle
13
Dies ist falsch: Number.MAX_VALUEist eine Ganzzahl. Es ist die Ganzzahl 17976931348623157 mit 292 Nullen danach. Der Grund , warum parseIntkehrt 1, weil sie erst in einen String sein Argument konvertiert und sucht dann die Zeichenfolge von links nach rechts. Sobald es das findet .(was keine Zahl ist), stoppt es.
Pauan
1
Übrigens, wenn Sie testen möchten, ob etwas eine Ganzzahl ist, verwenden Sie die ES6-Funktion Number.isInteger(foo). Da es jedoch noch nicht unterstützt wird, können Sie es Math.round(foo) === foostattdessen verwenden.
Pauan
2
@Pauan ist in Bezug auf die Implementierung Number.MAX_VALUEkeine Ganzzahl, sondern eine double. Es gibt also Folgendes: Ein Double kann jedoch eine Ganzzahl darstellen, da es zum Speichern von Ganzzahlen mit 32 Bit in JavaScript verwendet wird.
Alexis Wilke
1
@AlexisWilke Ja, natürlich implementiert JavaScript alle Zahlen als 64-Bit-Gleitkomma. Wenn mit "Ganzzahl" "32-Bit-Binär" gemeint Number.MAX_VALUEist, handelt es sich nicht um eine Ganzzahl. Aber wenn Sie mit "Ganzzahl" das mentale Konzept einer "Ganzzahl" meinen, dann ist es eine Ganzzahl. In JavaScript wird häufig die mentale Konzeptdefinition "Ganzzahl" verwendet, da alle Zahlen 64-Bit-Gleitkommazahlen sind.
Pauan
Es gibt auch, Number.MAX_SAFE_INTEGERaber das ist auch nicht die Nummer, nach der wir hier suchen.
tremby