Resolver o Javascript Promise fora do escopo da função

279

Eu tenho usado o ES6 Promise.

Normalmente, uma promessa é construída e usada assim

new Promise(function(resolve, reject){
    if (someCondition){
        resolve();
    } else {
        reject();
    } 
});

Mas eu tenho feito algo como abaixo para levar a decisão para fora por uma questão de flexibilidade.

var outsideResolve;
var outsideReject;
new Promise(function(resolve, reject) { 
    outsideResolve = resolve; 
    outsideReject = reject; 
});

E depois

onClick = function(){
    outsideResolve();
}

Isso funciona bem, mas existe uma maneira mais fácil de fazer isso? Caso contrário, é uma boa prática?

Morio
fonte
2
Eu não acho que exista outro caminho. Acredito que seja especificado que o retorno de chamada passado Promisedeve ser executado de forma síncrona para permitir "exportar" as duas funções.
Felix Kling
1
Isso funciona para mim exatamente como você escreveu. Então, para mim, esse é o caminho "canônico".
Gilad Barner
14
Eu acho que deveria haver uma maneira formal de conseguir isso no futuro. Esse recurso é muito poderoso na minha opinião, pois você pode esperar por valores de outros contextos.
Jose
Sempre que eles encontrarem uma solução adequada para esse problema, espero que eles também o façam funcionar com promessas aninhadas, algumas das quais podem se repetir.
Arthur Tarasov
Acho que a API do Promise "sugere" sempre usá-los como valores de retorno e nunca como objetos que você pode acessar ou chamar. Em outras palavras, nos força a tratá-los como valores de retorno em vez de objetos que podemos acessar ou funções que podemos chamar ou algo que podemos referenciar com uma variável ou passar como parâmetro etc. Se você começar a usar promessas como qualquer outro objeto, provavelmente irá acabam precisando resolvê-lo de fora, como na sua pergunta ... Dito isto, também acho que deveria haver uma maneira formal de fazer isso ... e o adiado parece apenas uma solução alternativa para mim.
Cancerbero 14/05/19

Respostas:

92

Não, não há outra maneira de fazer isso - a única coisa que posso dizer é que esse caso de uso não é muito comum. Como Felix disse no comentário - o que você faz sempre funcionará.

Vale ressaltar que a razão pela qual o construtor de promessas se comporta dessa maneira é a segurança de lançamento - se uma exceção que você não previu acontecer enquanto seu código estiver sendo executado dentro do construtor de promessa, ele se transformará em uma rejeição, essa forma de segurança de lançamento - convertendo erros lançados em rejeições é importante e ajuda a manter um código previsível.

Por esse motivo de segurança, o construtor da promessa foi escolhido em detrimento dos diferidos (que são uma maneira alternativa de construção da promessa que permite o que você está fazendo) - quanto às melhores práticas - eu passaria o elemento e usaria o construtor da promessa:

var p = new Promise(function(resolve, reject){
    this.onclick = resolve;
}.bind(this));

Por esse motivo - sempre que você puder usar o construtor de promessa sobre a exportação das funções - eu recomendo que você o utilize. Sempre que você puder evitar os dois - evite os dois e encadeie.

Observe que você nunca deve usar o construtor de promessas para coisas como if(condition), o primeiro exemplo pode ser escrito como:

var p = Promise[(someCondition)?"resolve":"reject"]();
Benjamin Gruenbaum
fonte
2
Oi Benjamin! Atualmente, não existe melhor maneira de obter açúcar promissor gostoso se ainda não sabemos quando a promessa será cumprida? Como algum tipo de padrão de espera / notificação assíncrona ? Como por exemplo, "loja", e depois chamar uma Promisecadeia? Por exemplo, no meu caso particular, estou em um servidor, aguardando uma resposta específica do cliente (um aperto de mão SYN-ACK-kinda para garantir que o cliente atualize com êxito o estado).
Domi
1
@ Domi confira q-connection e RxJS.
Benjamin Gruenbaum
2
Como eu poderia fazer o mesmo usando a API de busca?
Vinod Sobale 20/04
95
Não é comum? Acabo precisando de quase todos os projetos.
Tomáš Zato - Restabelece Monica
1
Quanto ao caso de uso, considere que você precisa fazer algo depois que um evento é disparado e algo mais aconteceu. Você quer transformar um evento em uma promessa e uni-lo a outra. Parece um problema genérico para mim.
Gherman
130

simples:

var promiseResolve, promiseReject;

var promise = new Promise(function(resolve, reject){
  promiseResolve = resolve;
  promiseReject = reject;
});

promiseResolve();
carter
fonte
2
@ruX, como a resposta aceita menciona - ela foi projetada dessa maneira de propósito. O ponto é que, se uma exceção for lançada, ela será capturada pelo construtor da promessa. Essa resposta (assim como a minha) tem a armadilha de lançar uma exceção para qualquer código que seja chamado promiseResolve(). A semântica de uma promessa é que ela sempre retorna um valor. Além disso, isso é funcionalmente o mesmo que o post do OP, não entendo que problema isso está resolvendo de maneira reutilizável.
Jon Jaques
4
@ JonJaques Não tenho certeza se o que você diz é verdade. O código que chama promiseResolve()não lançará uma exceção. Você pode definir um .catchno construtor e, não importa como o código o chame, o construtor .catchserá chamado. Aqui está o jsbin demonstrando como isso funciona: jsbin.com/yicerewivo/edit?js,console
carter
Sim, foi pego porque você envolveu outro construtor de promessas em torno dele - exatamente o que estou tentando fazer. No entanto, digamos que você tenha algum outro código que esteja tentando chamar resolve () fora do construtor (também conhecido como objeto adiado
9136 Jon Jaques
8
Eu nem tenho certeza se é um design ruim. Um erro lançado fora da promessa não deve ser capturado dentro da promessa. Talvez seja um exemplo de equívoco ou má compreensão, se o designer realmente espera que o erro seja detectado.
KalEl
3
Essa construção exata já é mencionada na pergunta. Você leu mesmo?
Cedric Reichenbach
103

Um pouco atrasado para a festa aqui, mas outra maneira de fazer isso seria usar um objeto adiado . Você tem essencialmente a mesma quantidade de clichê, mas é útil se você deseja contorná-los e possivelmente resolver fora de suas definições.

Implementação ingênua:

class Deferred {
  constructor() {
    this.promise = new Promise((resolve, reject)=> {
      this.reject = reject
      this.resolve = resolve
    })
  }
}

function asyncAction() {
  var dfd = new Deferred()

  setTimeout(()=> {
    dfd.resolve(42)
  }, 500)

  return dfd.promise
}

asyncAction().then(result => {
  console.log(result) // 42
})

Versão ES5:

function Deferred() {
  var self = this;
  this.promise = new Promise(function(resolve, reject) {
    self.reject = reject
    self.resolve = resolve
  })
}

function asyncAction() {
  var dfd = new Deferred()

  setTimeout(function() {
    dfd.resolve(42)
  }, 500)

  return dfd.promise
}

asyncAction().then(function(result) {
  console.log(result) // 42
})
Jon Jaques
fonte
1
Observe o escopo lexical aqui.
Florrie
1
Não há diferença prática se resolve|rejectsão atribuídos lexicamente ou por meio de bind. Esta é apenas uma implementação simples do objeto adiado jQuery que existe desde 1.0 (ish). Funciona exatamente como uma promessa, exceto que não há segurança no lançamento. O ponto principal desta questão era como salvar algumas linhas de código ao criar promessas.
Jon Jaques
1
Usando um diferido é a maneira usual de fazer isso, eu não tenho idéia por que isso não é mais elevado
BlueRaja - Danny Pflughoeft
1
Excelente resposta! Estava procurando a funcionalidade adiada que o jQuery oferece.
Anshul Koka #
2
Está Deferredobsoleto?
Pacerier 16/10
19

Uma solução criada em 2015 para minha estrutura. Eu chamei esse tipo de promessa de tarefa

function createPromise(handler){
  var _resolve, _reject;

  var promise = new Promise(function(resolve, reject){
    _resolve = resolve; 
    _reject = reject;

    handler(resolve, reject);
  })

  promise.resolve = _resolve;
  promise.reject = _reject;

  return promise;
}

var promise = createPromise()
promise.then(function(data){ alert(data) })

promise.resolve(200) // resolve from outside
Maxmaxmaximus
fonte
4
Obrigado, isso funcionou. Mas o que é manipulador? Eu tive que removê-lo para fazê-lo funcionar.
Sahid
16

Gostei da resposta do @JonJaques, mas queria dar um passo adiante.

Se você ligar thene, em catchseguida, o Deferredobjeto, ele implementará completamente a PromiseAPI e poderá tratá-la como promessa e awaitcomo tal.

class DeferredPromise {
  constructor() {
    this._promise = new Promise((resolve, reject) => {
      // assign the resolve and reject functions to `this`
      // making them usable on the class instance
      this.resolve = resolve;
      this.reject = reject;
    });
    // bind `then` and `catch` to implement the same interface as Promise
    this.then = this._promise.then.bind(this._promise);
    this.catch = this._promise.catch.bind(this._promise);
    this[Symbol.toStringTag] = 'Promise';
  }
}

const deferred = new DeferredPromise();
console.log('waiting 2 seconds...');
setTimeout(() => {
  deferred.resolve('whoa!');
}, 2000);

async function someAsyncFunction() {
  const value = await deferred;
  console.log(value);
}

someAsyncFunction();

Rico Kahler
fonte
10

Um método auxiliar aliviaria essa sobrecarga extra e proporcionaria a mesma sensação do jQuery.

function Deferred() {
    let resolve;
    let reject;
    const promise = new Promise((res, rej) => {
        resolve = res;
        reject = rej;
    });
    return { promise, resolve, reject };
}

O uso seria

const { promise, resolve, reject } = Deferred();
displayConfirmationDialog({
    confirm: resolve,
    cancel: reject
});
return promise;

O que é semelhante ao jQuery

const dfd = $.Deferred();
displayConfirmationDialog({
    confirm: dfd.resolve,
    cancel: dfd.reject
});
return dfd.promise();

Embora, em um caso de uso, essa sintaxe nativa simples seja adequada

return new Promise((resolve, reject) => {
    displayConfirmationDialog({
        confirm: resolve,
        cancel: reject
    });
});
Cory Danielson
fonte
8

Estou usando uma função auxiliar para criar o que chamo de "promessa simples" -

function flatPromise() {

    let resolve, reject;

    const promise = new Promise((res, rej) => {
      resolve = res;
      reject = rej;
    });

    return { promise, resolve, reject };
}

E eu estou usando assim -

function doSomethingAsync() {

    // Get your promise and callbacks
    const { resolve, reject, promise } = flatPromise();

    // Do something amazing...
    setTimeout(() => {
        resolve('done!');
    }, 500);

    // Pass your promise to the world
    return promise;

}

Veja o exemplo completo de trabalho -

Edit: Eu criei um pacote NPM chamado flat-promessa e o código também está disponível no GitHub .

Arik
fonte
7

Você pode agrupar a promessa em uma classe.

class Deferred {
    constructor(handler) {
        this.promise = new Promise((resolve, reject) => {
            this.reject = reject;
            this.resolve = resolve;
            handler(resolve, reject);
        });

        this.promise.resolve = this.resolve;
        this.promise.reject = this.reject;

        return this.promise;
    }
    promise;
    resolve;
    reject;
}

// How to use.
const promise = new Deferred((resolve, reject) => {
  // Use like normal Promise.
});

promise.resolve(); // Resolve from any context.
Hinrich
fonte
6

Muitas das respostas aqui são semelhantes ao último exemplo deste artigo . Estou armazenando em cache várias promessas e as funções resolve()e reject()podem ser atribuídas a qualquer variável ou propriedade. Como resultado, sou capaz de tornar esse código um pouco mais compacto:

function defer(obj) {
    obj.promise = new Promise((resolve, reject) => {
        obj.resolve = resolve;
        obj.reject  = reject;
    });
}

Aqui está um exemplo simplificado de uso desta versão de defer()para combinar uma FontFacecarga Promise com outro processo assíncrono:

function onDOMContentLoaded(evt) {
    let all = []; // array of Promises
    glob = {};    // global object used elsewhere
    defer(glob);
    all.push(glob.promise);
    // launch async process with callback = resolveGlob()

    const myFont = new FontFace("myFont", "url(myFont.woff2)");
    document.fonts.add(myFont);
    myFont.load();
    all.push[myFont];
    Promise.all(all).then(() => { runIt(); }, (v) => { alert(v); });
}
//...
function resolveGlob() {
    glob.resolve();
}
function runIt() {} // runs after all promises resolved 

Atualização: 2 alternativas, caso você queira encapsular o objeto:

function defer(obj = {}) {
    obj.promise = new Promise((resolve, reject) => {
        obj.resolve = resolve;
        obj.reject  = reject;
    });
    return obj;
}
let deferred = defer();

e

class Deferred {
    constructor() {
        this.promise = new Promise((resolve, reject) => {
            this.resolve = resolve;
            this.reject  = reject;
        });
    }
}
let deferred = new Deferred();
jamess
fonte
Se você estiver usando esses exemplos em uma função assíncrona, precisará consultar a propriedade da promessa, quando desejar usar o valor da promessa resolvida:const result = await deferred.promise;
b00t
6

A resposta aceita está errada. É muito fácil usar o escopo e as referências, embora possa irritar os puristas do Promise :

const createPromise = () => {
    let resolver;
    return [
        new Promise((resolve, reject) => {
            resolver = resolve;
        }),
        resolver,
    ];
};

const [ promise, resolver ] = createPromise();
promise.then(value => console.log(value));
setTimeout(() => resolver('foo'), 1000);

Essencialmente, estamos capturando a referência à função de resolução quando a promessa é criada e retornamos para que ela possa ser definida externamente.

Em um segundo, o console exibirá:

> foo
Todos
fonte
Eu acho que essa é a melhor abordagem. A única coisa é que o código pode ser um pouco menos detalhado.
pie6k 12/09/19
Agradável! Ideia inteligente. +50 se eu pudesse.
Mitya 12/03
Foi exatamente isso que o OP fez. De fato, você está reinventando o padrão diferido em relação às promessas, é claro que isso é possível e sua abordagem funciona (como o código OP inicial), mas essa não é a melhor prática devido a "motivo de segurança de lançamento" descrito na resposta aceita.
dhilt
4

Sim você pode. Usando a CustomEventAPI para o ambiente do navegador. E usando um projeto emissor de eventos nos ambientes node.js. Como o snippet na pergunta é para o ambiente do navegador, aqui está um exemplo prático para o mesmo.

function myPromiseReturningFunction(){
  return new Promise(resolve => {
    window.addEventListener("myCustomEvent", (event) => {
       resolve(event.detail);
    }) 
  })
}


myPromiseReturningFunction().then(result => {
   alert(result)
})

document.getElementById("p").addEventListener("click", () => {
   window.dispatchEvent(new CustomEvent("myCustomEvent", {detail : "It works!"}))
})
<p id="p"> Click me </p>

Espero que esta resposta seja útil!

Bhargav Ponnapalli
fonte
3

Nossa solução foi usar tampas para armazenar as funções de resolução / rejeição e anexar adicionalmente uma função para estender a promessa em si.

Aqui está o padrão:

function getPromise() {

    var _resolve, _reject;

    var promise = new Promise((resolve, reject) => {
        _reject = reject;
        _resolve = resolve;
    });

    promise.resolve_ex = (value) => {
       _resolve(value);
    };

    promise.reject_ex = (value) => {
       _reject(value);
    };

    return promise;
}

E usando-o:

var promise = getPromise();

promise.then(value => {
    console.info('The promise has been fulfilled: ' + value);
});

promise.resolve_ex('hello');  
// or the reject version 
//promise.reject_ex('goodbye');
Steven Spungin
fonte
2
Ótimo ... Estou apenas aprendendo promessas, mas sempre fiquei intrigado com o fato de você não parecer capaz de resolvê-las "em outro lugar". Usar um fechamento para ocultar os detalhes da implementação é uma ótima idéia ... mas, na verdade, não tenho certeza do que você fez: em vez de ter variáveis ​​privadas "pseudo", tenho certeza de que há uma maneira de ocultar completamente as variáveis que deve ser inacessível ... o que é realmente o que closures média ...
mike roedor
> Um fechamento é um bloco de código que pode ser referenciado (e transmitido) com acesso às variáveis ​​do escopo anexo. var _resolve, _reject; são o escopo anexo.
Steven Spungin
sim, justo o suficiente. Na verdade, parece-me que minha resposta é complicar demais as coisas e, além disso, que sua resposta pode ser simplificada: você só precisa ir promise.resolve_ex = _resolve; promise.reject_ex = _reject;... ainda funciona bem.
Mike rodent
" anexar uma função para estender a promessa em si. " - não faça isso. Promessas são valores de resultado, não devem fornecer a capacidade de resolvê-las. Você não quer passar por aqueles estendidos.
Bergi 04/07/19
2
A questão era como resolvê-lo fora do escopo. Aqui está uma solução que funciona e, em nossa produção, tivemos um motivo necessário para fazê-lo. Não vejo por que resolver o problema declarado merece um voto negativo.
Steven Spungin
2

Percebo que também sinto falta do padrão Adiado em certos casos. Você sempre pode criar uma sobre uma promessa do ES6:

export default class Deferred<T> {
    private _resolve: (value: T) => void = () => {};
    private _reject: (value: T) => void = () => {};

    private _promise: Promise<T> = new Promise<T>((resolve, reject) => {
        this._reject = reject;
        this._resolve = resolve;
    })

    public get promise(): Promise<T> {
        return this._promise;
    }

    public resolve(value: T) {
        this._resolve(value);
    }

    public reject(value: T) {
        this._reject(value);
    }
}
Carsten Hess
fonte
2

Obrigado a todos que postaram neste tópico. Criei um módulo que inclui o objeto Defer () descrito anteriormente, bem como alguns outros objetos criados sobre ele. Todos eles utilizam o Promises e a pura sintaxe de retorno de chamada do Promise para implementar a manipulação de comunicação / evento dentro de um programa.

  • Adiar: a promessa que pode ser resolvida falhou remotamente (fora do corpo)
  • Atraso: promessa que é resolvida automaticamente após um determinado período
  • Tempo limite: promessa que falha automaticamente após um determinado período.
  • Ciclo: promessa reativável para gerenciar eventos com a sintaxe Promise
  • Fila: fila de execução baseada no encadeamento Promise.

    rp = require("repeatable-promise")

    https://github.com/CABrouwers/repeatable-promise

CABrouwers
fonte
1

Eu escrevi uma pequena lib para isso. https://www.npmjs.com/package/@inf3rno/promise.exposed

Eu usei a abordagem método de fábrica outros escreveram, mas cancelou o then, catch,finally métodos também, então você pode resolver a promessa original por aqueles bem.

Resolvendo Promessa sem executor de fora:

const promise = Promise.exposed().then(console.log);
promise.resolve("This should show up in the console.");

Correndo com o setTimeout do executor de fora:

const promise = Promise.exposed(function (resolve, reject){
    setTimeout(function (){
        resolve("I almost fell asleep.")
    }, 100000);
}).then(console.log);

setTimeout(function (){
    promise.resolve("I don't want to wait that much.");
}, 100);

Existe um modo sem conflito se você não deseja poluir o espaço para nome global:

const createExposedPromise = require("@inf3rno/promise.exposed/noConflict");
const promise = createExposedPromise().then(console.log);
promise.resolve("This should show up in the console.");
inf3rno
fonte
1

Eu criei uma biblioteca chamada manual-promiseque funciona como uma gota em substituição a Promise. Nenhuma das outras respostas aqui funcionará como uma substituição nas substituições Promise, pois elas usam proxies ou wrappers.

yarn add manual-promise

npn install manual-promise


import { ManualPromise } from "manual-promise";

const prom = new ManualPromise();

prom.resolve(2);

// actions can still be run inside the promise
const prom2 = new ManualPromise((resolve, reject) => {
    // ... code
});


new ManualPromise() instanceof Promise === true

https://github.com/zpxp/manual-promise#readme

jeohd
fonte
0

Que tal criar uma função para seqüestrar a rejeição e devolvê-la?

function createRejectablePromise(handler) {
  let _reject;

  const promise = new Promise((resolve, reject) => {
    _reject = reject;

    handler(resolve, reject);
  })

  promise.reject = _reject;
  return promise;
}

// Usage
const { reject } = createRejectablePromise((resolve) => {
  setTimeout(() => {
    console.log('resolved')
    resolve();
  }, 2000)

});

reject();
Nikksan
fonte
0

Eu montei uma essência que faz esse trabalho: https://gist.github.com/thiagoh/c24310b562d50a14f3e7602a82b4ef13

aqui está como você deve usá-lo:

import ExternalizedPromiseCreator from '../externalized-promise';

describe('ExternalizedPromise', () => {
  let fn: jest.Mock;
  let deferredFn: jest.Mock;
  let neverCalledFn: jest.Mock;
  beforeEach(() => {
    fn = jest.fn();
    deferredFn = jest.fn();
    neverCalledFn = jest.fn();
  });

  it('resolve should resolve the promise', done => {
    const externalizedPromise = ExternalizedPromiseCreator.create(() => fn());

    externalizedPromise
      .promise
      .then(() => deferredFn())
      .catch(() => neverCalledFn())
      .then(() => {
        expect(deferredFn).toHaveBeenCalled();
        expect(neverCalledFn).not.toHaveBeenCalled();
        done();
      });

    expect(fn).toHaveBeenCalled();
    expect(neverCalledFn).not.toHaveBeenCalled();
    expect(deferredFn).not.toHaveBeenCalled();

    externalizedPromise.resolve();
  });
  ...
});
thiagoh
fonte
0

primeiro ative --allow-natives-syntax no navegador ou nó

const p = new Promise(function(resolve, reject){
    if (someCondition){
        resolve();
    } else {
        reject();
    } 
});

onClick = function () {
    %ResolvePromise(p, value)
}
Equitable
fonte
0

Apenas mais uma solução para resolver o Promise de fora

 class Lock {
        #lock;  // Promise to be resolved (on  release)
        release;  // Release lock
        id;  // Id of lock
        constructor(id) {
            this.id = id
            this.#lock = new Promise((resolve) => {
                this.release = () => {
                    if (resolve) {
                        resolve()
                    } else {
                        Promise.resolve()
                    }
                }
            })
        }
        get() { return this.#lock }
    }

Uso

let lock = new Lock(... some id ...);
...
lock.get().then(()=>{console.log('resolved/released')})
lock.release()  // Excpected 'resolved/released'
Egor Cherniaev
fonte