Résoudre la promesse Javascript hors de la portée de la fonction


280

J'utilise ES6 Promise.

Normalement, une promesse est construite et utilisée comme ceci

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

Mais j'ai fait quelque chose comme ci-dessous pour prendre la résolution à l'extérieur par souci de flexibilité.

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

Et ensuite

onClick = function(){
    outsideResolve();
}

Cela fonctionne bien, mais existe-t-il un moyen plus simple de le faire? Sinon, est-ce une bonne pratique?


2
Je ne pense pas qu'il existe un autre moyen. Je crois qu'il est spécifié que le rappel passé à Promisedoit être exécuté de manière synchrone pour permettre "d'exporter" les deux fonctions.
Felix Kling

1
Cela fonctionne pour moi exactement comme vous l'avez écrit. En ce qui me concerne, c'est la voie "canonique".
Gilad Barner

14
Je pense qu'il devrait y avoir un moyen officiel d'y parvenir à l'avenir. Cette fonctionnalité est très puissante à mon avis, car vous pouvez attendre des valeurs d'autres contextes.
Jose

Chaque fois qu'ils trouveront une solution appropriée à ce problème, j'espère qu'ils le feront également fonctionner pour des promesses imbriquées, dont certaines peuvent se reproduire.
Arthur Tarasov

Je pense que l'API Promise "suggère" de toujours les utiliser comme valeurs de retour et jamais comme des objets auxquels vous pouvez accéder ou appeler. En d'autres termes, nous obliger à les traiter comme des valeurs de retour au lieu d'objets auxquels nous pouvons accéder ou de fonctions que nous pouvons appeler ou quelque chose que nous pouvons référencer avec une variable ou passer en paramètre, etc. Si vous commencez à utiliser des promesses comme n'importe quel autre objet, vous le ferez probablement finir par avoir besoin de le résoudre de l'extérieur comme dans votre question ... Cela étant dit, je pense aussi qu'il devrait y avoir une façon formelle de le faire ... et différé semble juste une solution de contournement pour moi.
cancerbero

Réponses:


93

Non, il n'y a pas d'autre moyen de le faire - la seule chose que je peux dire, c'est que ce cas d'utilisation n'est pas très courant. Comme Felix l'a dit dans le commentaire - ce que vous faites fonctionnera toujours.

Il convient de mentionner que la raison pour laquelle le constructeur de promesses se comporte de cette façon est la sécurité des lancers - si une exception que vous ne pensiez pas se produit pendant que votre code s'exécute à l'intérieur du constructeur de promesses, cela se transformera en rejet, cette forme de sécurité des lancers - conversion des erreurs levées en les rejets sont importants et aident à maintenir un code prévisible.

Pour cette raison de sécurité, le constructeur de promesse a été choisi parmi les différés (qui sont une autre façon de construire la promesse qui permet ce que vous faites) - comme pour les meilleures pratiques - je passerais l'élément et utiliserais plutôt le constructeur de promesse:

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

Pour cette raison - chaque fois que vous pouvez utiliser le constructeur de promesses plutôt que d'exporter les fonctions - je vous recommande de l'utiliser. Chaque fois que vous pouvez éviter les deux - évitez les deux et la chaîne.

Notez que vous ne devriez jamais utiliser le constructeur de promesses pour des choses comme if(condition), le premier exemple pourrait être écrit comme suit:

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

2
Salut Benjamin! Existe-t-il actuellement un meilleur moyen d'obtenir du sucre promis délicieux si nous ne savons pas encore quand la promesse sera tenue? Comme une sorte de modèle d'attente / notification asynchrone ? Comme par exemple, "stocker", et invoquer plus tard une Promisechaîne? Par exemple, dans mon cas particulier, je suis sur un serveur, en attente d'une réponse spécifique du client (une poignée de main SYN-ACK-kinda pour vous assurer que le client est correctement mis à jour).
Domi

1
@Domi consultez q-connection et RxJS.
Benjamin Gruenbaum

2
Comment pourrais-je faire de même en utilisant l'API fetch?
Vinod Sobale

97
Pas commun? Je finis par en avoir besoin presque tous les projets.
Tomáš Zato - Rétablir Monica

1
En ce qui concerne le cas d'utilisation, considérez que vous devez faire quelque chose après le déclenchement d'un événement et autre chose. Vous voulez transformer un événement en promesse et l’unir à une autre promesse. Cela me semble être un problème générique.
Gherman

130

Facile:

var promiseResolve, promiseReject;

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

promiseResolve();

2
@ruX, comme le mentionne la réponse acceptée - il a été conçu de cette façon exprès. Le fait est que si une exception est levée, elle sera interceptée par le constructeur de la promesse. Cette réponse (ainsi que la mienne) a le piège de lever éventuellement une exception pour tous les appels de code promiseResolve(). La sémantique d'une promesse est qu'elle renvoie toujours une valeur. De plus, c'est la même chose que la publication de OP, je ne comprends pas quel problème cela résout de manière réutilisable.
Jon Jaques

4
@JonJaques Je ne sais pas si ce que vous dites est vrai. Le code qui appelle promiseResolve()ne lèvera pas d'exception. Vous pouvez définir un .catchsur le constructeur et quel que soit le code qui l'appelle, le constructeur .catchsera appelé. Voici le jsbin montrant comment cela fonctionne: jsbin.com/yicerewivo/edit?js,console
carter

Oui, c'est pris parce que vous avez entouré un autre constructeur de promesses - Exactement le point que j'essaie de faire valoir. Cependant, supposons que vous ayez un autre code qui essaie d'appeler resolver () en dehors du constructeur (aka objet différé) ... Il pourrait lever
Jon Jaques

8
Je ne suis même pas sûr que ce soit une mauvaise conception. Une erreur lancée en dehors de la promesse n'est pas censée être prise dans la promesse. C'est peut-être un exemple d'idée fausse ou de mauvaise compréhension, si le concepteur s'attend réellement à ce que l'erreur soit détectée.
KalEl

3
Cette construction exacte est déjà mentionnée dans la question. L'avez-vous même lu?
Cedric Reichenbach

103

Un peu tard pour la fête ici, mais une autre façon de le faire serait d'utiliser un objet différé . Vous avez essentiellement la même quantité de passe-partout, mais c'est pratique si vous voulez les faire circuler et éventuellement résoudre en dehors de leur définition.

Implémentation naïve:

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

Version 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
Notez la portée lexicale ici.
Florrie

1
Il n'y a pas de différence pratique quant à resolve|rejectleur attribution lexicale ou à travers bind. Il s'agit simplement d'une implémentation simple de l' objet jQuery Deferred qui existe depuis 1.0 (ish). Cela fonctionne exactement comme une promesse, sauf qu'il n'y a pas de sécurité contre les lancers. L'intérêt de cette question était de savoir comment enregistrer quelques lignes de code lors de la création de promesses.
Jon Jaques

1
Utiliser un différé est la façon habituelle de le faire, je ne sais pas pourquoi ce n'est pas plus élevé
BlueRaja - Danny Pflughoeft

1
Excellente réponse! Je cherchais la fonctionnalité différée qu'offre jQuery.
Anshul Koka

2
Est Deferredobsolète?
Pacerier

19

Une solution que j'ai trouvée en 2015 pour mon framework. J'ai appelé ce type de promesses Tâche

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
Merci, cela a fonctionné. Mais qu'est-ce que le gestionnaire? J'ai dû le retirer pour le faire fonctionner.
Sahid

16

J'ai aimé la réponse @JonJaques mais je voulais aller plus loin.

Si vous liez thenet catchensuite l' Deferredobjet, il met pleinement en œuvre l' PromiseAPI et vous pouvez le traiter comme promesse et awaitet tel.

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

Une méthode d'assistance atténuerait cette surcharge supplémentaire et vous donnerait la même sensation jQuery.

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

L'utilisation serait

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

Qui est similaire à jQuery

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

Bien que, dans un cas d'utilisation, cette syntaxe simple et native soit correcte

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

8

J'utilise une fonction d'aide pour créer ce que j'appelle une "promesse plate" -

function flatPromise() {

    let resolve, reject;

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

    return { promise, resolve, reject };
}

Et je l'utilise comme ça -

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;

}

Voir l'exemple de travail complet -

Edit: J'ai créé un package NPM appelé flat-promise et le code est également disponible sur GitHub .


7

Vous pouvez envelopper la promesse dans une 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

La plupart des réponses ici sont similaires au dernier exemple de cet article . Je mets en cache plusieurs promesses, et les fonctions resolve()et reject()peuvent être affectées à n'importe quelle variable ou propriété. En conséquence, je suis en mesure de rendre ce code légèrement plus compact:

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

Voici un exemple simplifié d'utilisation de cette version de defer()pour combiner une FontFacepromesse de chargement avec un autre processus asynchrone:

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 

Mise à jour: 2 alternatives au cas où vous souhaitez encapsuler l'objet:

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

et

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

Si vous utilisez ces exemples dans une fonction asynchrone, vous devrez vous référer à la propriété promise, lorsque vous souhaitez utiliser la valeur de la promesse résolue:const result = await deferred.promise;
b00t

6

La réponse acceptée est fausse. Il est assez facile d'utiliser la portée et les références, bien que cela puisse mettre les puristes de Promise en colère:

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

Nous saisissons essentiellement la référence à la fonction de résolution lorsque la promesse est créée, et nous la renvoyons pour qu'elle puisse être définie en externe.

En une seconde, la console affichera:

> foo

Je pense que c'est la meilleure approche. La seule chose est que le code pourrait être un peu moins détaillé.
pie6k

Agréable! Idée brillante. +50 si je pouvais.
Mitya

C'est exactement ce qu'OP a fait. En fait, vous réinventez le modèle différé sur les promesses, bien sûr, cela est possible et votre approche fonctionne (comme le code OP initial), mais ce n'est pas la meilleure pratique en raison de la "raison de sécurité de lancement" décrite dans la réponse acceptée.
dhilt

4

Oui, vous pouvez. En utilisant l' CustomEventAPI pour l'environnement du navigateur. Et en utilisant un projet d'émetteur d'événements dans les environnements node.js. Étant donné que l'extrait de code de la question concerne l'environnement du navigateur, voici un exemple de travail pour le même.

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>

J'espère que cette réponse est utile!


3

Notre solution consistait à utiliser des fermetures pour stocker les fonctions de résolution / rejet et à attacher en plus une fonction pour étendre la promesse elle-même.

Voici le schéma:

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

Et en l'utilisant:

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
Génial ... J'apprends juste des promesses mais j'ai toujours été intrigué par le fait que vous ne semblez pas en mesure de les résoudre "ailleurs". Utiliser une fermeture pour masquer les détails de l'implémentation est une excellente idée ... mais en fait, je ne suis pas sûr que c'est ce que vous avez fait: plutôt que d'avoir des "pseudo" variables privées, je suis presque sûr qu'il existe un moyen de masquer complètement les variables ce qui devrait être inaccessible ... c'est vraiment ce que signifient les fermetures ...
mike rodent

> Une fermeture est un bloc de code qui peut être référencé (et transmis) avec accès aux variables de la portée englobante. var _resolve, _reject; sont la portée englobante.
Steven Spungin

oui, assez juste. En fait, il me semble que ma réponse est trop compliquée, et en plus que votre réponse peut être simplifiée: il vous suffit de partir promise.resolve_ex = _resolve; promise.reject_ex = _reject;... fonctionne toujours bien.
mike rodent

" attache une fonction pour étendre la promesse elle-même. " - ne fais pas ça. Les promesses sont des valeurs de résultat, elles ne devraient pas permettre de les résoudre. Vous ne voulez pas passer ces extensions.
Bergi

2
La question était de savoir comment le résoudre en dehors du champ d'application. Voici une solution qui fonctionne, et dans notre production, nous avons effectivement eu une raison nécessaire de le faire. Je ne vois pas pourquoi la résolution du problème énoncé mérite un vote négatif.
Steven Spungin

2

Je me retrouve également à manquer le schéma différé dans certains cas. Vous pouvez toujours en créer un en plus d'une promesse 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

Merci à tous ceux qui ont posté dans ce fil. J'ai créé un module qui inclut l'objet Defer () décrit précédemment ainsi que quelques autres objets construits sur lui. Ils utilisent tous Promises et la syntaxe de rappel Promise pour implémenter la communication / la gestion des événements dans un programme.

  • Différer: la promesse qui peut être résolue a échoué à distance (en dehors de son corps)
  • Délai: promesse résolue automatiquement après un certain temps
  • TimeOut: promesse qui échoue automatiquement après un certain temps.
  • Cycle: promesse renouvelable de gérer les événements avec la syntaxe Promise
  • File d'attente: file d'attente d'exécution basée sur le chaînage Promise.

    rp = require("repeatable-promise")

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


1

J'ai écrit une petite lib pour ça. https://www.npmjs.com/package/@inf3rno/promise.exposed

Je l'approche d' autres méthode de l' usine ont écrit, mais je l' emportaient sur le then, catch, les finallyméthodes aussi, vous pouvez donc résoudre la promesse originelle de ceux aussi bien.

Résoudre la promesse sans exécuteur testamentaire de l'extérieur:

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

Course avec setTimeout de l'exécutif de l'extérieur:

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

Il existe un mode sans conflit si vous ne voulez pas polluer l'espace de noms global:

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

1

J'ai créé une bibliothèque appelée manual-promisequi fonctionne en remplacement de Promise. Aucune des autres réponses ici ne fonctionnera comme remplacement de remplacement pour Promise, car elles utilisent des proxy ou des 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


0

Que diriez-vous de créer une fonction pour détourner le rejet et le renvoyer?

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

J'ai mis en place un résumé qui fait ce travail: https://gist.github.com/thiagoh/c24310b562d50a14f3e7602a82b4ef13

voici comment l'utiliser:

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

activer d'abord --allow-natives-syntax sur le navigateur ou le nœud

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

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

0

Juste une autre solution pour résoudre Promise de l'extérieur

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

Usage

let lock = new Lock(... some id ...);
...
lock.get().then(()=>{console.log('resolved/released')})
lock.release()  // Excpected 'resolved/released'
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.