¿Es seguro resolver una promesa varias veces?

115

Tengo un servicio i18n en mi aplicación que contiene el siguiente código:

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

La idea es que el usuario llame ensureLocaleIsLoadedy espere a que se resuelva la promesa. Pero dado que el propósito de la función es solo garantizar que se cargue la configuración regional, estaría perfectamente bien que el usuario la invocara varias veces.

Actualmente solo estoy almacenando una única promesa y la resuelvo si el usuario vuelve a llamar a la función después de que la configuración regional se haya recuperado con éxito del servidor.

Por lo que puedo decir, esto está funcionando según lo previsto, pero me pregunto si es un enfoque adecuado.

Der Hochstapler
fuente
7
Vea esta respuesta .
robertklep
Yo también lo he usado y funciona bien.
Chandermani

Respuestas:

119

Según entiendo las promesas en la actualidad, esto debería estar bien al 100%. Lo único que hay que entender es que una vez resuelto (o rechazado), eso es todo para un objeto rechazado: está hecho.

Si debe then(...)volver a cumplir su promesa, debe obtener inmediatamente el (primer) resultado resuelto / rechazado.

Las llamadas adicionales a resolve()no tendrán ningún efecto (¿no deberían?). No estoy seguro de qué sucede si intenta rejectun objeto rechazado que fue previamente resolved(sospecho que nada).

demaniak
fuente
28
Aquí hay un JSBin que ilustra que todo lo anterior es realmente cierto: jsbin.com/gemepay/3/edit?js,console Solo se usa la primera resolución.
Konrad
4
¿Alguien ha encontrado alguna documentación oficial sobre esto? Por lo general, no es aconsejable depender de un comportamiento indocumentado incluso si funciona en este momento.
3oceno
ecma-international.org/ecma-262/6.0/#sec-promise.resolve - Hasta la fecha no he encontrado nada que indique que es inherentemente INSEGURO. Si su controlador hace algo que realmente solo debería hacerse UNA VEZ, haría que verifique y actualice algún estado antes de realizar la acción nuevamente. Pero también me gustaría alguna entrada oficial de MDN o un documento específico para obtener una claridad absoluta.
demaniak
No veo nada "preocupante" en la página de PromiseA +. Ver promisesaplus.com
demaniak
3
@demaniak Esta pregunta es sobre Promesas / A + , no promesas de ES6. Pero para responder a su pregunta, la parte de la especificación ES6 acerca de que la resolución / rechazo ajeno es seguro está aquí .
Trevor Robinson
1

Me enfrenté a lo mismo hace un tiempo, de hecho, una promesa solo se puede resolver una vez, otros intentos no harán nada (sin error, sin advertencia, sin theninvocación).

Decidí solucionarlo así:

getUsers(users => showThem(users));

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

simplemente pase su función como una devolución de llamada e invocala tantas veces como desee. Espero que tenga sentido.

Damiano
fuente
Creo que esto está mal. Simplemente puede devolver la promesa desde getUsersy luego invocar .then()esa promesa tantas veces como desee. No es necesario pasar una devolución de llamada. En mi opinión, una de las ventajas de las promesas es que no es necesario especificar la devolución de llamada por adelantado.
John Henckel
@JohnHenckel La idea es resolver la promesa varias veces, es decir, devolver datos varias veces, no tener varias .thendeclaraciones. Por lo que vale, creo que la única forma de devolver datos varias veces al contexto de llamada es usar devoluciones de llamada y no promesas, ya que las promesas no se crearon para funcionar de esa manera.
T. Rex
0

Si es necesario cambiar el valor de retorno de la promesa, devuelva nuevo valor de thencadena y al lado then/ catchen él

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
fuente
0

No hay una forma clara de resolver las promesas varias veces porque, dado que se resuelve, se hace. El mejor enfoque aquí es usar un patrón observable por el observador, por ejemplo, escribí el siguiente código que observa el evento del cliente de socket. Puede ampliar este código para satisfacer sus necesidades

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);
        }
    });
Николай Беспалов
fuente
0

Puede escribir pruebas para confirmar el comportamiento.

Al ejecutar la siguiente prueba, puede concluir que

La llamada a resolver () / rechazar () nunca arroja un error.

Una vez resuelto (rechazado), el valor resuelto (error rechazado) se conservará independientemente de las siguientes llamadas resolve () o rechazar ().

También puede consultar la publicación de mi blog para obtener más detalles.

/* 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
fuente
-1

Lo que debe hacer es poner un ng-if en su salida ng principal y mostrar una ruleta de carga en su lugar. Una vez que se carga la configuración regional, muestra la salida y deja que la jerarquía de componentes se represente. De esta manera, toda su aplicación puede asumir que la configuración regional está cargada y no es necesario realizar comprobaciones.

Marca Adrian
fuente