Risolvi Javascript Prometti al di fuori dell'ambito della funzione


280

Ho usato ES6 Promise.

Di solito, una Promessa viene costruita e utilizzata in questo modo

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

Ma ho fatto qualcosa di simile al di sotto per portare la risoluzione fuori per motivi di flessibilità.

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

E più tardi

onClick = function(){
    outsideResolve();
}

Funziona bene, ma c'è un modo più semplice per farlo? In caso contrario, è una buona pratica?


2
Non penso che ci sia un altro modo. Credo che sia specificato che il callback passato a Promisedeve essere eseguito in modo sincrono per consentire "l'esportazione" delle due funzioni.
Felix Kling,

1
Questo funziona per me esattamente come l'hai scritto tu. Per quanto mi riguarda, questo è il modo "canonico".
Gilad Barner,

14
Penso che ci dovrebbe essere un modo formale per raggiungere questo obiettivo in futuro. Questa funzione è molto potente secondo me in quanto puoi aspettare valori da altri contesti.
Jose

Ogni volta che trovano una soluzione adeguata a questo problema, spero che lo facciano funzionare anche per promesse annidate, alcune delle quali potrebbero ripresentarsi.
Arthur Tarasov,

Penso che l'API Promise "suggerisca" di usarli sempre come valori di ritorno e mai come oggetti a cui è possibile accedere o chiamare. In altre parole, ci costringono a trattarli come valori di ritorno anziché oggetti a cui possiamo accedere o funzioni che possiamo chiamare o qualcosa a cui possiamo fare riferimento con una variabile o passare come parametro, ecc. Se inizi a utilizzare le promesse come qualsiasi altro oggetto, probabilmente lo farai finire per aver bisogno di risolverlo dall'esterno come nella tua domanda ... Detto questo, penso anche che dovrebbe esserci un modo formale di farlo ... e Deferred sembra solo una soluzione alternativa per me.
cancerbero

Risposte:


93

No, non c'è altro modo per farlo - l'unica cosa che posso dire è che questo caso d'uso non è molto comune. Come ha detto Felix nel commento, ciò che fai funzionerà costantemente.

Vale la pena ricordare che il motivo per cui il costruttore di promesse si comporta in questo modo è la sicurezza di lancio - se un'eccezione che non hai previsto si verifica mentre il codice è in esecuzione all'interno del costruttore di promesse si trasformerà in un rifiuto, questa forma di sicurezza di lancio - convertendo gli errori generati in i rifiuti sono importanti e aiutano a mantenere un codice prevedibile.

Per questo motivo di sicurezza, il costruttore della promessa è stato scelto rispetto ai differiti (che sono un modo alternativo di costruzione della promessa che consente ciò che stai facendo) - come per le migliori pratiche - Passerei l'elemento e utilizzerei invece il costruttore della promessa:

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

Per questo motivo, ogni volta che è possibile utilizzare il costruttore promessa oltre all'esportazione delle funzioni, si consiglia di utilizzarlo. Ogni volta che puoi evitare entrambi - evita entrambi e la catena.

Nota che non dovresti mai usare il costruttore di promesse per cose come if(condition), il primo esempio potrebbe essere scritto come:

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

2
Ciao Benjamin! Al momento non esiste un modo migliore per ottenere lo zucchero promettente delizioso se non sappiamo ancora quando la promessa sarà mantenuta? Come una sorta di modello di attesa / notifica asincrono ? Come ad esempio "store", e in seguito invocare una Promisecatena? Ad esempio, nel mio caso particolare, sono su un server, in attesa di una risposta specifica del client (una stretta di mano SYN-ACK-kinda per assicurarsi che il client sia stato aggiornato correttamente).
Domi,

1
@Domi controlla q-connection e RxJS.
Benjamin Gruenbaum,

2
Come potrei fare lo stesso usando l'API di recupero?
Vinod Sobale,

96
Non comune? Finisco per averne bisogno quasi ogni progetto.
Tomáš Zato - Ripristina Monica il

1
Per quanto riguarda il caso d'uso, devi fare qualcosa dopo che un evento è stato attivato e qualcos'altro è successo. Vuoi trasformare un evento in una promessa e unirlo con un'altra promessa. Mi sembra un problema generico.
Gherman,

130

semplice:

var promiseResolve, promiseReject;

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

promiseResolve();

2
@ruX, Come menzionato nella risposta accettata, è stato progettato appositamente in questo modo. Il punto è che se viene generata un'eccezione, verrà catturata dal costruttore della promessa. Questa risposta (così come la mia) ha il rischio di gettare un'eccezione per qualsiasi chiamata in codice promiseResolve(). La semantica di una promessa è che restituisce sempre un valore. Anche questo è funzionalmente uguale al post di OP, non capisco quale problema risolva in modo riutilizzabile.
Jon Jaques,

4
@JonJaques Non sono sicuro che ciò che dici sia vero. Il codice che chiama promiseResolve()non genererà un'eccezione. Puoi definire a .catchsul costruttore e indipendentemente dal codice che lo chiama, .catchverrà chiamato il costruttore . Ecco il jsbin che dimostra come funziona: jsbin.com/yicerewivo/edit?js,console
carter

Sì, è preso perché hai avvolto un altro costruttore di promesse attorno ad esso - Esattamente il punto che sto cercando di fare. Tuttavia, supponiamo che tu abbia qualche altro codice che sta provando a chiamare resol () al di fuori del costruttore (aka oggetto rinviato
Jon Jaques,

8
Non sono nemmeno sicuro che sia un cattivo design. Un errore lanciato fuori dalla promessa non dovrebbe essere colto all'interno della promessa. È forse un esempio di malinteso o cattiva comprensione, se il progettista si aspetta effettivamente che l'errore venga colto al suo interno.
KalEl,

3
Questo costrutto esatto è già menzionato nella domanda. Lo hai almeno letto?
Cedric Reichenbach,

103

Un po 'tardi alla festa qui, ma un altro modo per farlo sarebbe usare un oggetto differito . Hai essenzialmente la stessa quantità di boilerplate, ma è utile se vuoi passarli in giro e possibilmente risolverli al di fuori della loro definizione.

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

Versione 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
Si noti qui l'ambito lessicale.
Florrie,

1
Non vi è alcuna differenza pratica nel fatto che resolve|rejectsiano assegnati in modo lessicale o passivo bind. Questa è solo una semplice implementazione dell'oggetto jQuery Deferred che esiste da 1.0 (ish). Funziona esattamente come una promessa, tranne per il fatto che non esiste sicurezza di lancio. Il punto centrale di questa domanda era come salvare alcune righe di codice durante la creazione di promesse.
Jon Jaques,

1
Usare un differito è il solito modo per farlo, non ho idea del perché questo non sia più alto
BlueRaja - Danny Pflughoeft

1
Risposta eccellente! Stavo cercando la funzionalità differita che offre jQuery.
Anshul Koka,

2
È Deferreddeprecato?
Pacerier,

19

Una soluzione che ho trovato nel 2015 per il mio framework. Ho chiamato questo tipo di promesse Task

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

4
Grazie, ha funzionato. Ma cos'è l'handler? Ho dovuto rimuoverlo per farlo funzionare.
Sahid,

16

Mi è piaciuta la risposta di @JonJaques ma volevo fare un ulteriore passo avanti.

Se associ thene catchquindi l' Deferredoggetto, implementa completamente l' PromiseAPI e puoi trattarlo come promessa e awaitcosì e così.

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 metodo di aiuto allevierebbe questo sovraccarico in più e ti darebbe la stessa sensazione jQuery.

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

L'utilizzo sarebbe

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

Che è simile a jQuery

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

Sebbene, in un caso d'uso, questa sintassi nativa semplice vada bene

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

8

Sto usando una funzione di aiuto per creare quella che chiamo una "promessa piatta" -

function flatPromise() {

    let resolve, reject;

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

    return { promise, resolve, reject };
}

E lo sto usando così -

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;

}

Vedi esempio di lavoro completo -

Modifica: ho creato un pacchetto NPM chiamato flat-promise e il codice è disponibile anche su GitHub .


7

Puoi racchiudere la Promessa in una 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.

6

Molte delle risposte qui sono simili all'ultimo esempio in questo articolo . Sto memorizzando nella cache più promesse e le funzioni resolve()e reject()possono essere assegnate a qualsiasi variabile o proprietà. Di conseguenza sono in grado di rendere questo codice leggermente più compatto:

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

Ecco un esempio semplificato dell'utilizzo di questa versione di defer()per combinare una FontFacepromessa di carico con un altro processo asincrono:

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 

Aggiornamento: 2 alternative nel caso in cui si desideri incapsulare l'oggetto:

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

Se stai usando questi esempi in una funzione asincrona, dovrai fare riferimento alla proprietà promise, quando vuoi usare il valore della promessa risolta:const result = await deferred.promise;
b00t

6

La risposta accettata è sbagliata È abbastanza facile usare l'ambito e i riferimenti, anche se può far arrabbiare i puristi di 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);

Fondamentalmente stiamo afferrando il riferimento alla funzione di risoluzione quando viene creata la promessa e lo restituiamo in modo che possa essere impostato esternamente.

In un secondo la console produrrà:

> foo

Penso che questo sia l'approccio migliore. L'unica cosa è che il codice potrebbe essere un po 'meno dettagliato.
pie6k,

Bello! Idea intelligente. +50 se potessi.
Mitya

Questo è esattamente ciò che ha fatto OP. In effetti stai reinventando il modello differito rispetto alle promesse, ovviamente questo è possibile e il tuo approccio funziona (come il codice OP iniziale), ma questa non è la migliore pratica a causa del "motivo di sicurezza" descritto nella risposta accettata.
Dhilt

4

Si, puoi. Utilizzando l' CustomEventAPI per l'ambiente del browser. E usando un progetto di emettitore di eventi in ambienti node.js. Poiché lo snippet nella domanda riguarda l'ambiente del browser, ecco un esempio funzionante per lo stesso.

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>

Spero che questa risposta sia utile!


3

La nostra soluzione era quella di utilizzare le chiusure per memorizzare le funzioni di risoluzione / rifiuto e inoltre allegare una funzione per estendere la promessa stessa.

Ecco lo schema:

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 usandolo:

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
Fantastico ... sto solo imparando le promesse, ma sono stato costantemente sconcertato dal fatto che tu non sembri essere in grado di risolverle "altrove". Usare una chiusura per nascondere i dettagli dell'implementazione è un'ottima idea ... ma in realtà non sono sicuro che sia quello che hai fatto: piuttosto che avere variabili private "pseudo" sono abbastanza sicuro che c'è un modo per nascondere completamente le variabili che dovrebbe essere inaccessibile ... che è davvero ciò che significa chiusure ...
Mike Rodent,

> Una chiusura è un blocco di codice a cui può essere fatto riferimento (e passato) con accesso alle variabili dell'ambito che lo racchiude. var _resolve, _reject; sono l'ambito allegato.
Steven Spungin,

sì, abbastanza giusto. In realtà mi sembra che la mia risposta sia eccessivamente complicata, e inoltre che la tua risposta possa essere semplificata: devi solo andare promise.resolve_ex = _resolve; promise.reject_ex = _reject;... funziona ancora bene.
mike rodent,

" associa una funzione per estendere la promessa stessa " - non farlo. Le promesse sono valori di risultato, non dovrebbero fornire la capacità di risolverli. Non vuoi passare quelli estesi in giro.
Bergi,

2
La domanda era come risolverlo al di fuori dell'ambito. Ecco una soluzione che funziona e nella nostra produzione abbiamo effettivamente avuto un motivo necessario per farlo. Non vedo perché la soluzione del problema dichiarato meriti un voto negativo.
Steven Spungin,

2

Mi ritrovo a mancare anche il modello differito in alcuni casi. Puoi sempre crearne uno in cima a una promessa 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

Grazie a tutti coloro che hanno pubblicato in questa discussione. Ho creato un modulo che include l'oggetto Defer () descritto in precedenza, nonché alcuni altri oggetti costruiti su di esso. Tutti sfruttano Promise e la sintassi di richiamo Promise per implementare la gestione di eventi / comunicazioni all'interno di un programma.

  • Rinvia: promessa che può essere risolta fallita da remoto (al di fuori del suo corpo)
  • Ritardo: promessa che viene risolta automaticamente dopo un determinato tempo
  • TimeOut: promessa che fallisce automaticamente dopo un determinato tempo.
  • Ciclo: promessa di riattivazione per la gestione degli eventi con la sintassi Promise
  • Coda: coda di esecuzione basata sul concatenamento Promise.

    rp = require("repeatable-promise")

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


1

Ho scritto una piccola lib per questo. https://www.npmjs.com/package/@inf3rno/promise.exposed

Ho usato l'approccio metodo factory altri hanno scritto, ma ho sovrascritto then, catch, finallymetodi anche, in modo è possibile risolvere la promessa originale di quelli pure.

Risoluzione della promessa senza esecutore dall'esterno:

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

Correre con il set dell'esecutore Timeout dall'esterno:

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

Esiste una modalità senza conflitto se non si desidera inquinare lo spazio dei nomi globale:

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

1

Ho creato una libreria chiamata manual-promiseche funziona come una goccia in sostituzione di Promise. Nessuna delle altre risposte qui funzionerà come calo delle sostituzioni per Promise, poiché usano proxy o wrapper.

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

Che ne dici di creare una funzione per dirottare il rifiuto e restituirlo?

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

Ho messo insieme una sostanza che fa quel lavoro: https://gist.github.com/thiagoh/c24310b562d50a14f3e7602a82b4ef13

ecco come dovresti 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

per prima cosa abilitare la sintassi --allow-natives sul browser o sul nodo

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

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

0

Solo un'altra soluzione per risolvere Promessa dall'esterno

 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'
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.