Resolver Javascript Promise fuera del alcance de la función


280

He estado usando ES6 Promise.

Por lo general, una promesa se construye y se usa así

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

Pero he estado haciendo algo como a continuación para tomar la resolución afuera en aras de la flexibilidad.

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

Y después

onClick = function(){
    outsideResolve();
}

Esto funciona bien, pero ¿hay alguna manera más fácil de hacerlo? Si no, ¿es esta una buena práctica?


2
No creo que haya otra forma. Creo que se especifica que la devolución de llamada que se pasa Promisedebe ejecutarse sincrónicamente para permitir "exportar" las dos funciones.
Felix Kling

1
Esto funciona para mí exactamente como lo escribiste. Por lo que a mí respecta, esta es la forma "canónica".
Gilad Barner

14
Creo que debería haber una forma formal de lograr esto en el futuro. Esta característica es muy poderosa en mi opinión, ya que puede esperar valores de otros contextos.
José

Siempre que encuentren una solución adecuada a este problema, espero que también lo hagan funcionar para promesas anidadas, algunas de las cuales pueden repetirse.
Arthur Tarasov

Creo que la API de Promise "sugiere" usarlos siempre como valores de retorno y nunca como objetos a los que pueda acceder o llamar. En otras palabras, nos obligan a tratarlos como valores de retorno en lugar de objetos a los que podemos acceder o funciones a las que podemos llamar o algo a lo que podamos hacer referencia con una variable o pasar como parámetro, etc. Si comienza a usar promesas como cualquier otro objeto, probablemente lo hará terminan necesitando resolverlo desde afuera como en su pregunta ... Dicho esto, también creo que debería haber una forma formal de hacer esto ... y Diferido parece solo una solución para mí.
cancerbero

Respuestas:


93

No, no hay otra forma de hacer esto; lo único que puedo decir es que este caso de uso no es muy común. Como dijo Félix en el comentario, lo que hagas funcionará constantemente.

Vale la pena mencionar que la razón por la cual el constructor de promesas se comporta de esta manera es la seguridad de lanzamiento: si una excepción que no anticipó sucede mientras su código se ejecuta dentro del constructor de promesas, se convertirá en un rechazo, esta forma de seguridad de lanzamiento: la conversión de errores arrojados a Los rechazos son importantes y ayudan a mantener un código predecible.

Por esta razón de seguridad de lanzamiento, el constructor de la promesa se eligió entre los diferidos (que son una forma alternativa de construcción de la promesa que permite lo que está haciendo), en cuanto a las mejores prácticas, pasaría el elemento y usaría el constructor de la promesa en su lugar:

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

Por esta razón, siempre que pueda usar el constructor de promesas antes de exportar las funciones, le recomiendo que lo use. Siempre que pueda evitar ambos, evite ambos y la cadena.

Tenga en cuenta que nunca debe usar el constructor de promesas para cosas como if(condition), el primer ejemplo podría escribirse como:

var p = Promise[(someCondition)?"resolve":"reject"]();

2
Hola benjamin ¿No hay actualmente una mejor manera de obtener azúcar de promesa deliciosa si aún no sabemos cuándo se cumplirá la promesa? ¿Te gusta algún tipo de patrón asíncrono de espera / notificación ? Como por ejemplo, "almacenar", y luego invocar una Promisecadena? Por ejemplo, en mi caso particular, estoy en un servidor, esperando una respuesta específica del cliente (un apretón de manos SYN-ACK para asegurarme de que el cliente se haya actualizado correctamente).
Domi

1
@Domi echa un vistazo a q-connection y RxJS.
Benjamin Gruenbaum

2
¿Cómo podría hacer lo mismo usando fetch API?
Vinod Sobale

96
¿No es común? Termino necesitándolo en casi todos los proyectos.
Tomáš Zato - Restablece a Monica el

1
En cuanto al caso de uso, considere que debe hacer algo después de que se desencadena un evento y sucede algo más. Desea transformar el evento en una promesa y unirlo con otra promesa. Me parece un problema genérico.
Gherman

130

sencillo:

var promiseResolve, promiseReject;

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

promiseResolve();

2
@ruX, como menciona la respuesta aceptada, se diseñó de esta manera a propósito. El punto es que si se lanza una excepción, será atrapada por el constructor de la promesa. Esta respuesta (así como la mía) tiene el peligro de posiblemente lanzar una excepción para cualquier código que llame promiseResolve(). La semántica de una promesa es que siempre devuelve un valor. Además, esto es funcionalmente lo mismo que la publicación de OP, no entiendo qué problema está resolviendo de una manera reutilizable.
Jon Jaques

44
@JonJaques No estoy seguro de si lo que dices es cierto. El código que llama promiseResolve()no arrojará una excepción. Puede definir a .catchen el constructor y no importa qué código lo llame, se llamará al constructor .catch. Aquí está el jsbin que demuestra cómo funciona esto: jsbin.com/yicerewivo/edit?js,console
carter el

Sí, está atrapado porque envolviste a otro constructor de promesas, exactamente el punto que estoy tratando de hacer. Sin embargo, supongamos que tiene algún otro código que está tratando de llamar a resolve () fuera del constructor (también conocido como objeto diferido) ... Podría arrojar una excepción y no quedar atrapado jsbin.com/cokiqiwapo/1/edit?js,console
Jon Jaques

8
Ni siquiera estoy seguro de que sea un mal diseño. Se supone que un error arrojado fuera de la promesa no debe quedar atrapado dentro de la promesa. Es quizás un ejemplo de error o mal entendimiento, si el diseñador realmente espera que el error quede atrapado dentro.
KalEl

3
Esta construcción exacta ya se menciona en la pregunta. ¿Lo leíste siquiera?
Cedric Reichenbach

103

Un poco tarde para la fiesta aquí, pero otra forma de hacerlo sería usar un objeto diferido . Básicamente, tiene la misma cantidad de repetitivo, pero es útil si desea pasarlos y posiblemente resolverlos fuera de su definición.

Implementación ingenua:

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

Versión 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
})

1
Observe el alcance léxico aquí.
Florrie

1
No hay diferencia práctica en si resolve|rejectse asignan léxicamente o por medio bind. Esta es solo una implementación simple del objeto jQuery Deferred que ha existido desde 1.0 (ish). Funciona exactamente como una promesa, excepto que no hay seguridad de tiro. El punto central de esta pregunta era cómo guardar algunas líneas de código al crear promesas.
Jon Jaques

1
Usar un diferido es la forma habitual de hacer esto, no tengo idea de por qué esto no es más alto
BlueRaja - Danny Pflughoeft

1
Excelente respuesta! Estaba buscando la funcionalidad diferida que ofrece jQuery.
Anshul Koka

2
¿Está en Deferreddesuso?
Pacerier

19

Una solución que se me ocurrió en 2015 para mi marco. Llamé a este tipo de promesas Tarea

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

44
Gracias, esto funcionó. Pero, ¿qué es el controlador? Tuve que eliminarlo para que funcione.
Sahid

16

Me gustó la respuesta de @JonJaques pero quería ir un paso más allá.

Si vincula theny catchluego el Deferredobjeto, entonces implementa completamente la PromiseAPI y puede tratarlo como una promesa y awaiteso y 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();


10

Un método auxiliar aliviaría esta sobrecarga adicional y le daría la misma sensación de jQuery.

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

El uso sería

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

Que es similar a jQuery

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

Aunque, en un caso de uso, esta sintaxis nativa simple está bien

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

8

Estoy usando una función auxiliar para crear lo que llamo una "promesa plana":

function flatPromise() {

    let resolve, reject;

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

    return { promise, resolve, reject };
}

Y lo estoy usando así ...

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;

}

Ver ejemplo de trabajo completo:

Editar: he creado un paquete NPM llamado flat-promise y el código también está disponible en GitHub .


7

Puedes envolver la promesa en una clase.

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.

6

Muchas de las respuestas aquí son similares al último ejemplo en este artículo . Estoy almacenando en caché varias promesas, y las funciones resolve()y reject()se pueden asignar a cualquier variable o propiedad. Como resultado, puedo hacer que este código sea un poco más compacto:

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

Aquí hay un ejemplo simplificado del uso de esta versión defer()para combinar una FontFacePromesa de carga con otro proceso así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 

Actualización: 2 alternativas en caso de que desee encapsular el objeto:

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

y

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

Si está utilizando estos ejemplos en una función asíncrona, deberá consultar la propiedad de la promesa, cuando desee utilizar el valor de la promesa resuelta:const result = await deferred.promise;
b00t

6

La respuesta aceptada es incorrecta. Es bastante fácil usar el alcance y las referencias, aunque puede enojar a los puristas de 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);

Básicamente, estamos tomando la referencia a la función resolver cuando se crea la promesa, y la devolvemos para que se pueda establecer externamente.

En un segundo, la consola mostrará:

> foo

Creo que este es el mejor enfoque. Lo único es que el código podría ser un poco menos detallado.
pie6k

¡Agradable! Idea inteligente. +50 si pudiera.
Mitia

Esto es justo lo que hizo OP. De hecho, está reinventando el patrón diferido sobre las promesas, por supuesto, esto es posible y su enfoque funciona (como el código OP inicial), pero esta no es la mejor práctica debido a la "razón de seguridad de lanzamiento" descrita en la respuesta aceptada.
dhilt

4

Sí tu puedes. Mediante el uso de la CustomEventAPI para el entorno del navegador. Y utilizando un proyecto de emisor de eventos en entornos node.js. Dado que el fragmento en la pregunta es para el entorno del navegador, aquí hay un ejemplo de trabajo para el mismo.

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 respuesta sea útil!


3

Nuestra solución fue utilizar cierres para almacenar las funciones de resolución / rechazo y, además, adjuntar una función para ampliar la promesa en sí.

Aquí está el patrón:

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

Y usándolo:

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

2
Genial ... Solo estoy aprendiendo Promesas, pero me ha intrigado constantemente el hecho de que no pareces poder resolverlas "en otro lugar". Usar un cierre para ocultar los detalles de la implementación es una gran idea ... pero, de hecho, no estoy seguro de que haya hecho eso: en lugar de tener "pseudo" variables privadas, estoy bastante seguro de que hay una manera de ocultar completamente las variables que debería ser inaccesible ... que es realmente lo que cierres media ...
Mike roedores

> Un cierre es un bloque de código al que se puede hacer referencia (y pasar) con acceso a las variables del ámbito de inclusión. var _resolve, _reject; son el alcance de cierre.
Steven Spungin

Sí, lo suficientemente justo. En realidad, me parece que mi respuesta es complicar las cosas y, además, su respuesta puede simplificarse: solo necesita ir promise.resolve_ex = _resolve; promise.reject_ex = _reject;... todavía funciona bien.
Mike roedor

" adjunte una función para extender la promesa en sí misma " . No haga eso. Las promesas son valores de resultados, no deben proporcionar la capacidad de resolverlas. No quieres pasar esos extendidos.
Bergi

2
La pregunta era cómo resolverlo fuera del alcance. Aquí hay una solución que funciona, y en nuestra producción hemos tenido una razón necesaria para hacerlo. No veo por qué resolver el problema planteado merece un voto negativo.
Steven Spungin

2

También me encuentro perdiendo el patrón diferido en ciertos casos. Siempre puede crear uno encima de una promesa 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);
    }
}

2

Gracias a todos los que publicaron en este hilo. Creé un módulo que incluye el objeto Defer () descrito anteriormente, así como algunos otros objetos construidos sobre él. Todos aprovechan las promesas y la clara sintaxis de devolución de llamada de Promise para implementar el manejo de comunicación / eventos dentro de un programa.

  • Aplazar: la promesa que se puede resolver falló de forma remota (fuera de su cuerpo)
  • Retraso: promesa que se resuelve automáticamente después de un tiempo determinado
  • TimeOut: promesa que falla automáticamente después de un tiempo determinado.
  • Ciclo: promesa reactivable para administrar eventos con la sintaxis Promise
  • Cola: Cola de ejecución basada en el encadenamiento de Promesa.

    rp = require("repeatable-promise")

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


1

Escribí una pequeña lib para esto. https://www.npmjs.com/package/@inf3rno/promise.exposed

He utilizado el enfoque método de fábrica otros escribieron, pero hizo caso omiso de la then, catch,finally métodos también, para que puedan resolver la promesa original de esos también.

Resolviendo Promesa sin albacea desde afuera:

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

Competir con el setTimeout del ejecutor desde afuera:

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

Hay un modo sin conflicto si no desea contaminar el espacio de nombres global:

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

1

Hice una biblioteca llamada manual-promiseque funciona como una caída en reemplazo de Promise. Ninguna de las otras respuestas aquí funcionará como una caída en los reemplazos Promise, ya que usan proxies o contenedores.

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


0

¿Qué tal crear una función para secuestrar el rechazo y devolverlo?

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

0

He reunido una esencia que hace ese trabajo: https://gist.github.com/thiagoh/c24310b562d50a14f3e7602a82b4ef13

así es como debe usarlo:

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

0

primero habilite --allow-natives-syntax en el navegador o nodo

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

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

0

Solo otra solución para resolver Promise desde el exterior

 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'
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.