Como fazer uma promessa com setTimeout

91

Este não é um problema do mundo real, estou apenas tentando entender como as promessas são criadas.

Preciso entender como fazer uma promessa para uma função que não retorna nada, como setTimeout.

Suponha que eu tenha:

function async(callback){ 
    setTimeout(function(){
        callback();
    }, 5000);
}

async(function(){
    console.log('async called back');
});

Como faço para criar uma promessa que asyncpode retornar após o setTimeoutestá pronto para callback()?

Acho que embrulhar me levaria a algum lugar:

function setTimeoutReturnPromise(){

    function promise(){}

    promise.prototype.then = function() {
        console.log('timed out');
    };

    setTimeout(function(){
        return ???
    },2000);


    return promise;
}

Mas não consigo pensar além disso.

laggingreflex
fonte
Você está tentando criar sua própria biblioteca de promessas?
TJ Crowder
@TJCrowder Não era, mas acho que era isso que eu estava tentando entender. É assim que uma biblioteca faria
laggingreflex
@ lagging: Faz sentido, adicionei um exemplo de implementação de promessa básica à resposta.
TJ Crowder
Acho que este é um problema do mundo real e que eu tive que resolver para um grande projeto que minha empresa estava construindo. Provavelmente havia maneiras melhores de fazer isso, mas eu essencialmente precisava atrasar a resolução de uma promessa por causa de nossa pilha bluetooth. Vou postar abaixo para mostrar o que fiz.
sunny-mittal
1
Apenas uma observação que em 2017 'async' é um nome um tanto confuso para uma função, como você deve terasync function async(){...}
mikemaccana

Respostas:

121

Atualização (2017)

Aqui em 2017, as Promises são integradas ao JavaScript, foram adicionadas pela especificação ES2015 (polyfills estão disponíveis para ambientes desatualizados como IE8-IE11). A sintaxe usada usa um retorno de chamada que você passa para o Promiseconstrutor (o Promise executor ) que recebe as funções para resolver / rejeitar a promessa como argumentos.

Primeiro, como asyncagora tem um significado em JavaScript (mesmo que seja apenas uma palavra-chave em certos contextos), vou usar latercomo o nome da função para evitar confusão.

Atraso Básico

Usando promessas nativas (ou um polyfill fiel) ficaria assim:

function later(delay) {
    return new Promise(function(resolve) {
        setTimeout(resolve, delay);
    });
}

Observe que isso pressupõe uma versão setTimeoutcompatível com a definição de navegadores onde setTimeoutnão passa nenhum argumento para o retorno de chamada, a menos que você os forneça após o intervalo (isso pode não ser verdade em ambientes sem navegador, e não costumava ser verdade no Firefox, mas agora; é verdade no Chrome e até mesmo no IE8).

Atraso básico com valor

Se quiser que sua função passe opcionalmente um valor de resolução, em qualquer navegador vagamente moderno que permite fornecer argumentos extras para setTimeoutapós o atraso e, em seguida, passa-os para o retorno de chamada quando chamado, você pode fazer isso (Firefox e Chrome atuais; IE11 + , presumivelmente Edge; não IE8 ou IE9, nenhuma ideia sobre o IE10):

function later(delay, value) {
    return new Promise(function(resolve) {
        setTimeout(resolve, delay, value); // Note the order, `delay` before `value`
        /* Or for outdated browsers that don't support doing that:
        setTimeout(function() {
            resolve(value);
        }, delay);
        Or alternately:
        setTimeout(resolve.bind(null, value), delay);
        */
    });
}

Se você estiver usando as funções de seta ES2015 +, isso pode ser mais conciso:

function later(delay, value) {
    return new Promise(resolve => setTimeout(resolve, delay, value));
}

ou mesmo

const later = (delay, value) =>
    new Promise(resolve => setTimeout(resolve, delay, value));

Atraso cancelável com valor

Se você deseja possibilitar o cancelamento do timeout, não pode simplesmente devolver uma promessa de later, porque as promessas não podem ser canceladas.

Mas podemos facilmente retornar um objeto com um cancelmétodo e um acessador para a promessa e rejeitar a promessa no cancelamento:

const later = (delay, value) => {
    let timer = 0;
    let reject = null;
    const promise = new Promise((resolve, _reject) => {
        reject = _reject;
        timer = setTimeout(resolve, delay, value);
    });
    return {
        get promise() { return promise; },
        cancel() {
            if (timer) {
                clearTimeout(timer);
                timer = 0;
                reject();
                reject = null;
            }
        }
    };
};

Exemplo ao vivo:

const later = (delay, value) => {
    let timer = 0;
    let reject = null;
    const promise = new Promise((resolve, _reject) => {
        reject = _reject;
        timer = setTimeout(resolve, delay, value);
    });
    return {
        get promise() { return promise; },
        cancel() {
            if (timer) {
                clearTimeout(timer);
                timer = 0;
                reject();
                reject = null;
            }
        }
    };
};

const l1 = later(100, "l1");
l1.promise
  .then(msg => { console.log(msg); })
  .catch(() => { console.log("l1 cancelled"); });

const l2 = later(200, "l2");
l2.promise
  .then(msg => { console.log(msg); })
  .catch(() => { console.log("l2 cancelled"); });
setTimeout(() => {
  l2.cancel();
}, 150);


Resposta Original de 2014

Normalmente, você terá uma biblioteca de promessas (uma que você mesmo escreve ou uma das várias por aí). Essa biblioteca geralmente terá um objeto que você pode criar e "resolver" posteriormente, e esse objeto terá uma "promessa" que você poderá obter dele.

Então later, tenderia a ser algo assim:

function later() {
    var p = new PromiseThingy();
    setTimeout(function() {
        p.resolve();
    }, 2000);

    return p.promise(); // Note we're not returning `p` directly
}

Em um comentário sobre a pergunta, perguntei:

Você está tentando criar sua própria biblioteca de promessas?

E você disse

Eu não estava, mas acho que agora é isso que eu estava tentando entender. É assim que uma biblioteca faria

Para ajudar nesse entendimento, aqui está um exemplo muito básico , que não é remotamente compatível com Promises-A: Live Copy

<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Very basic promises</title>
</head>
<body>
  <script>
    (function() {

      // ==== Very basic promise implementation, not remotely Promises-A compliant, just a very basic example
      var PromiseThingy = (function() {

        // Internal - trigger a callback
        function triggerCallback(callback, promise) {
          try {
            callback(promise.resolvedValue);
          }
          catch (e) {
          }
        }

        // The internal promise constructor, we don't share this
        function Promise() {
          this.callbacks = [];
        }

        // Register a 'then' callback
        Promise.prototype.then = function(callback) {
          var thispromise = this;

          if (!this.resolved) {
            // Not resolved yet, remember the callback
            this.callbacks.push(callback);
          }
          else {
            // Resolved; trigger callback right away, but always async
            setTimeout(function() {
              triggerCallback(callback, thispromise);
            }, 0);
          }
          return this;
        };

        // Our public constructor for PromiseThingys
        function PromiseThingy() {
          this.p = new Promise();
        }

        // Resolve our underlying promise
        PromiseThingy.prototype.resolve = function(value) {
          var n;

          if (!this.p.resolved) {
            this.p.resolved = true;
            this.p.resolvedValue = value;
            for (n = 0; n < this.p.callbacks.length; ++n) {
              triggerCallback(this.p.callbacks[n], this.p);
            }
          }
        };

        // Get our underlying promise
        PromiseThingy.prototype.promise = function() {
          return this.p;
        };

        // Export public
        return PromiseThingy;
      })();

      // ==== Using it

      function later() {
        var p = new PromiseThingy();
        setTimeout(function() {
          p.resolve();
        }, 2000);

        return p.promise(); // Note we're not returning `p` directly
      }

      display("Start " + Date.now());
      later().then(function() {
        display("Done1 " + Date.now());
      }).then(function() {
        display("Done2 " + Date.now());
      });

      function display(msg) {
        var p = document.createElement('p');
        p.innerHTML = String(msg);
        document.body.appendChild(p);
      }
    })();
  </script>
</body>
</html>
TJ Crowder
fonte
modernjavascript.blogspot.com/2013/08/… related :)
Benjamin Gruenbaum
sua resposta não suporta cancelTimeout
Alexander Danilov
@AlexanderDanilov: As promessas não podem ser canceladas. Você certamente poderia escrever uma função que retornasse um objeto com um método de cancelamento e, separadamente, um acessador para a promessa e, em seguida, rejeitar a promessa se o método de cancelamento fosse chamado ...
TJ Crowder
1
@AlexanderDanilov: Eu fui em frente e adicionei um.
TJ Crowder
0

Esta não é uma resposta à pergunta original. Mas, como uma pergunta original não é um problema do mundo real, não deve ser um problema. Tentei explicar a um amigo o que são promessas em JavaScript e a diferença entre promessa e retorno de chamada.

O código abaixo serve como explicação:

//very basic callback example using setTimeout
//function a is asynchronous function
//function b used as a callback
function a (callback){
    setTimeout (function(){
       console.log ('using callback:'); 
       let mockResponseData = '{"data": "something for callback"}'; 
       if (callback){
          callback (mockResponseData);
       }
    }, 2000);

} 

function b (dataJson) {
   let dataObject = JSON.parse (dataJson);
   console.log (dataObject.data);   
}

a (b);

//rewriting above code using Promise
//function c is asynchronous function
function c () {
   return new Promise(function (resolve, reject) {
     setTimeout (function(){
       console.log ('using promise:'); 
       let mockResponseData = '{"data": "something for promise"}'; 
       resolve(mockResponseData); 
    }, 2000);      
   }); 

}

c().then (b);

JsFiddle

Yurin
fonte