Est-il sûr de résoudre une promesse plusieurs fois?

115

J'ai un service i18n dans mon application qui contient le code suivant:

var i18nService = function() {
  this.ensureLocaleIsLoaded = function() {
    if( !this.existingPromise ) {
      this.existingPromise = $q.defer();

      var deferred = this.existingPromise;
      var userLanguage = $( "body" ).data( "language" );
      this.userLanguage = userLanguage;

      console.log( "Loading locale '" + userLanguage + "' from server..." );
      $http( { method:"get", url:"/i18n/" + userLanguage, cache:true } ).success( function( translations ) {
        $rootScope.i18n = translations;
        deferred.resolve( $rootScope.i18n );
      } );
    }

    if( $rootScope.i18n ) {
      this.existingPromise.resolve( $rootScope.i18n );
    }

    return this.existingPromise.promise;
  };

L'idée est que l'utilisateur appelle ensureLocaleIsLoadedet attend que la promesse soit résolue. Mais étant donné que le but de la fonction est uniquement de s'assurer que la locale est chargée, il serait parfaitement normal que l'utilisateur l'invoque plusieurs fois.

Je suis actuellement en train de stocker une seule promesse et de la résoudre si l'utilisateur appelle à nouveau la fonction après que les paramètres régionaux ont été récupérés avec succès sur le serveur.

D'après ce que je peux dire, cela fonctionne comme prévu, mais je me demande si c'est une approche appropriée.

Der Hochstapler
la source
7
Voyez cette réponse .
robertklep
Moi aussi je l'ai utilisé et cela fonctionne très bien.
Chandermani

Réponses:

119

Si je comprends bien les promesses actuelles, cela devrait être parfait à 100%. La seule chose à comprendre est qu'une fois résolue (ou rejetée), c'est pour un objet différé - c'est fait.

Si vous devez appeler à nouveau then(...)sur sa promesse, vous devriez immédiatement obtenir le (premier) résultat résolu / rejeté.

Les appels supplémentaires à resolve()n'auront pas (ne devraient pas?) Avoir d'effet. Je ne sais pas ce qui se passe si vous essayez rejectun objet différé qui était auparavant resolved(je ne soupçonne rien).

demaniak
la source
28
Voici un JSBin illustrant que tout ce qui précède est en fait vrai: jsbin.com/gemepay/3/edit?js,console Seule la première résolution est utilisée.
konrad
4
Quelqu'un a-t-il trouvé une documentation officielle à ce sujet? Il est généralement déconseillé de s'appuyer sur un comportement non documenté, même si cela fonctionne maintenant.
3ocène le
ecma-international.org/ecma-262/6.0/#sec-promise.resolve - À ce jour, je n'ai rien trouvé qui indique qu'il est intrinsèquement UNSAFE. Si votre gestionnaire fait quelque chose qui ne devrait vraiment être fait qu'UNE SEULE FOIS, je lui demanderais de vérifier et de mettre à jour un état avant d'exécuter à nouveau l'action. Mais j'aimerais aussi une entrée MDN officielle ou un document de spécification pour obtenir une clarté absolue.
demaniak
Je ne vois rien de "troublant" dans la page PromiseA +. Voir promisesaplus.com
demaniak
3
@demaniak Cette question concerne les promesses / A + , pas les promesses ES6. Mais pour répondre à votre question, la partie de la spécification ES6 sur la sécurité / résolution superflue est ici .
Trevor Robinson
1

J'ai fait face à la même chose il y a quelque temps, en effet une promesse ne peut être résolue qu'une fois, un autre essai ne fera rien (pas d'erreur, pas d'avertissement, pas d' theninvocation).

J'ai décidé de travailler comme ceci:

getUsers(users => showThem(users));

getUsers(callback){
    callback(getCachedUsers())
    api.getUsers().then(users => callback(users))
}

passez simplement votre fonction en rappel et invoquez-la autant de fois que vous le souhaitez! J'espère que cela a du sens.

Damiano
la source
Je pense que cela est faux. Vous pouvez simplement renvoyer la promesse getUserset l'invoquer .then()autant de fois que vous le souhaitez. Il n'est pas nécessaire de passer un rappel. À mon avis, l'un des avantages des promesses est que vous n'avez pas besoin de spécifier le rappel à l'avance.
John Henckel
@JohnHenckel L'idée est de résoudre la promesse plusieurs fois, c'est-à-dire de renvoyer des données plusieurs fois, de ne pas avoir plusieurs .thendéclarations. Pour ce que cela vaut, je pense que la seule façon de renvoyer des données plusieurs fois au contexte appelant est d'utiliser des rappels et non des promesses, car les promesses n'ont pas été conçues pour fonctionner de cette manière.
T. Rex le
0

Si vous avez besoin de modifier la valeur de retour de la promesse, renvoyez simplement la nouvelle valeur thenet enchaînez ensuite then/ catchdessus

var p1 = new Promise((resolve, reject) => { resolve(1) });
    
var p2 = p1.then(v => {
  console.log("First then, value is", v);
  return 2;
});
    
p2.then(v => {
  console.log("Second then, value is", v);
});

Buksy
la source
0

Il n'y a pas de moyen clair de résoudre les promesses plusieurs fois, car depuis qu'elles sont résolues, c'est fait. La meilleure approche ici consiste à utiliser un modèle observable par l'observateur, par exemple, j'ai écrit le code suivant qui observe l'événement du client socket. Vous pouvez étendre ce code pour répondre à vos besoins

const evokeObjectMethodWithArgs = (methodName, args) => (src) => src[methodName].apply(null, args);
    const hasMethodName = (name) => (target = {}) => typeof target[name] === 'function';
    const Observable = function (fn) {
        const subscribers = [];
        this.subscribe = subscribers.push.bind(subscribers);
        const observer = {
            next: (...args) => subscribers.filter(hasMethodName('next')).forEach(evokeObjectMethodWithArgs('next', args))
        };
        setTimeout(() => {
            try {
                fn(observer);
            } catch (e) {
                subscribers.filter(hasMethodName('error')).forEach(evokeObjectMethodWithArgs('error', e));
            }
        });

    };

    const fromEvent = (target, eventName) => new Observable((obs) => target.on(eventName, obs.next));

    fromEvent(client, 'document:save').subscribe({
        async next(document, docName) {
            await writeFilePromise(resolve(dataDir, `${docName}`), document);
            client.emit('document:save', document);
        }
    });
Николай Беспалов
la source
0

Vous pouvez écrire des tests pour confirmer le comportement.

En exécutant le test suivant, vous pouvez conclure que

L'appel de résolution () / rejet () ne génère jamais d'erreur.

Une fois réglée (rejetée), la valeur résolue (erreur rejetée) sera conservée indépendamment des appels à résoudre () ou à rejeter () suivants.

Vous pouvez également consulter mon article de blog pour plus de détails.

/* eslint-disable prefer-promise-reject-errors */
const flipPromise = require('flip-promise').default

describe('promise', () => {
    test('error catch with resolve', () => new Promise(async (rs, rj) => {
        const getPromise = () => new Promise(resolve => {
            try {
                resolve()
            } catch (err) {
                rj('error caught in unexpected location')
            }
        })
        try {
            await getPromise()
            throw new Error('error thrown out side')
        } catch (e) {
            rs('error caught in expected location')
        }
    }))
    test('error catch with reject', () => new Promise(async (rs, rj) => {
        const getPromise = () => new Promise((_resolve, reject) => {
            try {
                reject()
            } catch (err) {
                rj('error caught in unexpected location')
            }
        })
        try {
            await getPromise()
        } catch (e) {
            try {
                throw new Error('error thrown out side')
            } catch (e){
                rs('error caught in expected location')
            }
        }
    }))
    test('await multiple times resolved promise', async () => {
        const pr = Promise.resolve(1)
        expect(await pr).toBe(1)
        expect(await pr).toBe(1)
    })
    test('await multiple times rejected promise', async () => {
        const pr = Promise.reject(1)
        expect(await flipPromise(pr)).toBe(1)
        expect(await flipPromise(pr)).toBe(1)
    })
    test('resolve multiple times', async () => {
        const pr = new Promise(resolve => {
            resolve(1)
            resolve(2)
            resolve(3)
        })
        expect(await pr).toBe(1)
    })
    test('resolve then reject', async () => {
        const pr = new Promise((resolve, reject) => {
            resolve(1)
            resolve(2)
            resolve(3)
            reject(4)
        })
        expect(await pr).toBe(1)
    })
    test('reject multiple times', async () => {
        const pr = new Promise((_resolve, reject) => {
            reject(1)
            reject(2)
            reject(3)
        })
        expect(await flipPromise(pr)).toBe(1)
    })

    test('reject then resolve', async () => {
        const pr = new Promise((resolve, reject) => {
            reject(1)
            reject(2)
            reject(3)
            resolve(4)
        })
        expect(await flipPromise(pr)).toBe(1)
    })
test('constructor is not async', async () => {
    let val
    let val1
    const pr = new Promise(resolve => {
        val = 1
        setTimeout(() => {
            resolve()
            val1 = 2
        })
    })
    expect(val).toBe(1)
    expect(val1).toBeUndefined()
    await pr
    expect(val).toBe(1)
    expect(val1).toBe(2)
})

})
transang
la source
-1

Ce que vous devez faire est de mettre un ng-if sur votre ng-outlet principal et d'afficher un spinner de chargement à la place. Une fois que vos paramètres régionaux sont chargés, vous affichez la prise et laissez la hiérarchie des composants s'afficher. De cette façon, toute votre application peut supposer que les paramètres régionaux sont chargés et qu'aucune vérification n'est nécessaire.

Marque Adrian
la source