Por que setTimeout () “interrompe” para grandes valores de atraso de milissegundos?

104

Eu me deparei com um comportamento inesperado ao passar um grande valor em milissegundos para setTimeout(). Por exemplo,

setTimeout(some_callback, Number.MAX_VALUE);

e

setTimeout(some_callback, Infinity);

ambos fazem some_callbackcom que seja executado quase imediatamente, como se eu tivesse passado em 0vez de um grande número como o atraso.

Por que isso acontece?

Matt Ball
fonte

Respostas:

143

Isso se deve ao setTimeout usando um int de 32 bits para armazenar o atraso, de modo que o valor máximo permitido seja

2147483647

se você tentar

2147483648

você consegue seu problema ocorrendo.

Só posso presumir que isso está causando alguma forma de exceção interna no JS Engine e fazendo com que a função seja acionada imediatamente, em vez de não disparar.

Um disparo
fonte
1
Ok, isso faz sentido. Eu estou supondo que na verdade não levanta uma exceção interna. Em vez disso, vejo que (1) está causando um estouro de inteiro ou (2) coagindo internamente o atraso para um valor int de 32 bits sem sinal. Se (1) for o caso, estou realmente passando um valor negativo para o atraso. Se for (2), algo semelhante delay >>> 0acontece, então o atraso passado é zero. De qualquer forma, o fato de o atraso ser armazenado como um int sem sinal de 32 bits explica esse comportamento. Obrigado!
Matt Ball
Atualização antiga, mas acabei de descobrir que o limite máximo é 49999861776383( 49999861776384faz com que o retorno de chamada seja
acionado
7
@maxp Isso porque49999861776383 % 2147483648 === 2147483647
David Da Silva Contín
@DavidDaSilvaContín muito tarde para isso, mas pode me explicar melhor? Não consigo entender por que 2147483647 não é o limite?
Nick Coad
2
@NickCoad ambos os números atrasariam a mesma quantidade (ou seja, 49999861776383 é o mesmo que 2147483647 de um ponto de vista de 32 bits com sinal). escreva-os em binário e pegue os últimos 31 bits, todos serão 1s.
Mark Fisher
24

Você pode 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
fonte
2
isso é legal, mas perdemos a capacidade de usar ClearTimeout devido à recursão.
Allan Nienhuis
2
Você realmente não perde a capacidade de cancelá-lo, desde que faça sua contabilidade e substitua o timeoutId que deseja cancelar dentro desta função.
charlag
23

Algumas explicações aqui: http://closure-library.googlecode.com/svn/docs/closure_goog_timer_timer.js.source.html

Valores de tempo limite muito grandes para caber em um inteiro de 32 bits assinado podem causar estouro no FF, Safari e Chrome, resultando no tempo limite sendo agendado imediatamente. Faz mais sentido simplesmente não agendar esses tempos limites, já que 24,8 dias está além da expectativa razoável para o navegador permanecer aberto.

warpech
fonte
2
A resposta da warpech faz muito sentido - um processo de longa execução como um servidor Node.JS pode soar como uma exceção, mas para ser honesto, se você tem algo que deseja garantir que aconteça em exatamente 24 e poucos dias com precisão de milissegundos então você deve usar algo mais robusto em face de erros de servidor e máquina do que setTimeout ...
cfogelberg
@cfogelberg, não vi o FF ou qualquer outra implementação do setTimeout(), mas espero que eles calculem a data e a hora em que ele deve acordar e não decrementem um contador em algum tique definido aleatoriamente ... (Pode-se esperar , pelo menos)
Alexis Wilke
2
Estou executando Javascript em NodeJS em um servidor, 24,8 dias ainda é bom, mas estou procurando uma maneira mais lógica de definir um retorno de chamada para acontecer em, digamos, 1 mês (30 dias). Qual seria o caminho a percorrer para isso?
Paul
1
Tenho, com certeza, as janelas do navegador abertas há mais de 24,8 dias. É bizarro para mim que os navegadores não façam internamente algo como a solução de Ronen, pelo menos até MAX_SAFE_INTEGER
acjay
1
Quem diz? Eu mantenho meu navegador aberto por mais de 24 dias ...;)
Pete Alvin
2

Verifique a documentação do nó sobre Timers aqui: https://nodejs.org/api/timers.html (assumindo o mesmo em js, já que é um termo tão onipresente agora baseado em loop de evento

Em resumo:

Quando o atraso for maior que 2147483647 ou menor que 1, o atraso será definido como 1.

e o atraso é:

O número de milissegundos a aguardar antes de chamar o retorno de chamada.

Parece que seu valor de tempo limite está sendo padronizado para um valor inesperado ao longo dessas regras, possivelmente?

SillyGilly
fonte
1

Tropecei nisso quando tentei fazer logout automático de um usuário com uma sessão expirada. Minha solução foi apenas zerar o tempo limite após um dia e manter a funcionalidade para usar o clearTimeout.

Aqui está um pequeno exemplo de protótipo:

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

E você pode limpá-lo com o stopTimermétodo:

timer.stopTimer();
Tim
fonte
0

Não posso comentar senão responder a todas as pessoas. Leva um valor sem sinal (você não pode esperar milissegundos negativos obviamente) Então, como o valor máximo é "2147483647", quando você insere um valor mais alto, ele começa a partir de 0.

Basicamente, atraso = {VALUE}% 2147483647.

Portanto, usar o atraso de 2147483648 aumentaria 1 milissegundo, portanto, proc instantâneo.

KYGAS
fonte
-2
Number.MAX_VALUE

na verdade não é um número inteiro. O valor máximo permitido para setTimeout é provavelmente 2 ^ 31 ou 2 ^ 32. Experimentar

parseInt(Number.MAX_VALUE) 

e você recebe 1 de volta em vez de 1,7976931348623157e + 308.

Osmund
fonte
13
Isso está incorreto: Number.MAX_VALUEé um número inteiro. É o número inteiro 17976931348623157 com 292 zeros depois. O motivo do parseIntretorno 1é porque ele primeiro converte seu argumento em uma string e, em seguida, pesquisa a string da esquerda para a direita. Assim que encontrar o .(que não é um número), ele para.
Pauan
1
A propósito, se você quiser testar se algo é um inteiro, use a função ES6 Number.isInteger(foo). Mas, como ainda não é compatível, você pode usar Math.round(foo) === foo.
Pauan
2
@Pauan, em termos de implementação, Number.MAX_VALUEnão é um inteiro, mas um double. Portanto, é isso ... Um double pode representar um inteiro, pois é usado para salvar inteiros de 32 bits em JavaScript.
Alexis Wilke
1
@AlexisWilke Sim, é claro que o JavaScript implementa todos os números como ponto flutuante de 64 bits. Se por "inteiro" você quer dizer "binário de 32 bits", então Number.MAX_VALUEnão é um inteiro. Mas se por "inteiro" você quer dizer o conceito mental de "um inteiro", então é um inteiro. Em JavaScript, como todos os números são de ponto flutuante de 64 bits, é comum usar a definição de conceito mental de "inteiro".
Pauan
Há também, Number.MAX_SAFE_INTEGERmas esse não é o número que estamos procurando aqui.
trêmulo