É seguro resolver uma promessa várias vezes?

115

Tenho um serviço i18n em meu aplicativo que contém o seguinte 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;
  };

A ideia é que o usuário ligasse ensureLocaleIsLoadede esperasse o cumprimento da promessa. Mas, dado que o propósito da função é apenas garantir que o local seja carregado, seria perfeitamente normal que o usuário a chamasse várias vezes.

No momento, estou apenas armazenando uma única promessa e resolvo isso se o usuário chamar a função novamente depois que a localidade for recuperada com êxito do servidor.

Pelo que posso dizer, está funcionando conforme o esperado, mas estou me perguntando se essa é uma abordagem adequada.

Der Hochstapler
fonte
7
Veja esta resposta .
robertklep
Eu também usei e funciona bem.
Chandermani

Respostas:

119

Pelo que entendi as promessas no momento, isso deve ser 100% bom. A única coisa a entender é que uma vez resolvido (ou rejeitado), isso é para um objeto adiado - está feito.

Se você ligar then(...)para a promessa novamente, você deve obter imediatamente o (primeiro) resultado resolvido / rejeitado.

Chamadas adicionais para resolve()não terão (não deveriam?) Ter qualquer efeito. Não tenho certeza do que acontece se você tentar rejectum objeto adiado que era anteriormente resolved(não suspeito de nada).

Demaniak
fonte
28
Aqui está um JSBin ilustrando que tudo acima é realmente verdade: jsbin.com/gemepay/3/edit?js,console Somente a primeira resolução é usada.
Konrad
4
Alguém encontrou alguma documentação oficial sobre isso? Geralmente é desaconselhável confiar em comportamento não documentado, mesmo que funcione agora.
3oceno,
ecma-international.org/ecma-262/6.0/#sec-promise.resolve - Até o momento, não encontrei nada que afirmasse ser intrinsecamente INSEGURO. Se o seu manipulador fizer algo que realmente deveria ser feito UMA VEZ, eu faria com que ele verificasse e atualizasse algum estado antes de executar a ação novamente. Mas também gostaria de alguma entrada oficial do MDN ou documento de especificações para obter clareza absoluta.
demaniak
Não consigo ver nada "preocupante" na página PromiseA +. Consulte promisesaplus.com
demaniak
3
@demaniak Esta questão é sobre Promises / A + , não sobre promessas ES6. Mas para responder à sua pergunta, a parte da especificação ES6 sobre resolução / rejeição externa segura está aqui .
Trevor Robinson
1

Eu enfrentei a mesma coisa há um tempo, na verdade uma promessa só pode ser resolvida uma vez, outras tentativas não farão nada (nenhum erro, nenhum aviso, nenhuma theninvocação).

Decidi contornar assim:

getUsers(users => showThem(users));

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

apenas passe sua função como um retorno de chamada e invoque quantas vezes desejar! Espero que isso faça sentido.

Damiano
fonte
Acho que isso está errado. Você pode simplesmente devolver a promessa getUserse invocar .then()essa promessa quantas vezes quiser. Não há necessidade de passar um retorno de chamada. Na minha opinião, uma das vantagens das promessas é que você não precisa especificar o retorno de chamada antecipadamente.
John Henckel
@JohnHenckel A ideia é resolver a promessa várias vezes, ou seja, retornar dados várias vezes, não ter várias .theninstruções. Pelo que vale a pena, acho que a única maneira de retornar dados várias vezes para o contexto de chamada é usar retornos de chamada e não promessas, já que as promessas não foram criadas para funcionar dessa maneira.
T. Rex
0

Se você precisar alterar o valor de retorno da promessa, simplesmente retorne o novo valor thene encadeie a seguir then/ catchnele

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

Não há uma maneira clara de resolver as promessas várias vezes porque, uma vez que está resolvido, está feito. A melhor abordagem aqui é usar o padrão observável pelo observador, por exemplo, eu escrevi a seguir o código que observa o evento do cliente de soquete. Você pode estender este código para atender às suas necessidades

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

Você pode escrever testes para confirmar o comportamento.

Executando o seguinte teste, você pode concluir que

A chamada resolve () / rejeitar () nunca lança um erro.

Depois de liquidado (rejeitado), o valor resolvido (erro rejeitado) será preservado independentemente das seguintes chamadas resolve () ou rejeitar ().

Você também pode verificar minha postagem do blog para obter detalhes.

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

O que você deve fazer é colocar um ng-if na saída principal do ng e mostrar um botão giratório de carregamento. Depois que sua localidade for carregada, você mostra a saída e deixa a hierarquia do componente renderizar. Dessa forma, todo o seu aplicativo pode assumir que a localidade está carregada e nenhuma verificação é necessária.

Adrian Brand
fonte