Безопасно ли выполнять обещание несколько раз?

115

В моем приложении есть служба i18n, содержащая следующий код:

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

Идея состоит в том, что пользователь позвонит ensureLocaleIsLoadedи будет ждать выполнения обещания. Но, учитывая, что цель функции состоит только в том, чтобы гарантировать, что локаль загружена, для пользователя было бы вполне нормально вызвать ее несколько раз.

В настоящее время я просто сохраняю одно обещание и разрешаю его, если пользователь снова вызовет функцию после того, как языковой стандарт был успешно получен с сервера.

Насколько я могу судить, это работает как задумано, но мне интересно, правильный ли это подход.

Der Hochstapler
источник
7
Смотрите этот ответ .
Робертклеп 02
Я тоже его использовал, и он отлично работает.
Чандермани 02

Ответы:

119

Насколько я понимаю в настоящее время обещания, это должно быть на 100% нормально. Единственное, что нужно понять, - это то, что однажды решено (или отклонено), то есть для отложенного объекта - это сделано.

Если вы then(...)снова вызовете это обещание, вы должны немедленно получить (первый) разрешенный / отклоненный результат.

Дополнительные вызовы resolve()не будут (не должны?) Иметь никакого эффекта. Не уверен, что произойдет, если вы попытаетесь rejectиспользовать отложенный объект, который был ранее resolved(я ничего не подозреваю).

Деманиак
источник
28
Вот JSBin, иллюстрирующий, что все вышеперечисленное действительно верно: jsbin.com/gemepay/3/edit?js,console Когда-либо используется только первое разрешение.
Конрад
4
Кто-нибудь нашел официальную документацию по этому поводу? Обычно не рекомендуется полагаться на недокументированное поведение, даже если оно работает прямо сейчас.
3ocene 05
ecma-international.org/ecma-262/6.0/#sec-promise.resolve - на сегодняшний день я не нашел ничего, что заявляло бы, что это НЕ БЕЗОПАСНО. Если ваш обработчик делает что-то, что действительно следует делать только ОДИН РАЗ, я бы попросил его проверить и обновить какое-то состояние перед повторным выполнением действия. Но я также хотел бы, чтобы некоторая официальная запись MDN или спецификация были абсолютно ясны.
demaniak
Не вижу ничего "тревожного" на странице PromiseA +. См promisesaplus.com
demaniak
3
@demaniak Этот вопрос касается обещаний / A + , а не обещаний ES6. Но чтобы ответить на ваш вопрос, то часть ES6 спецификации о посторонней решимости / отклонять быть безопасно здесь .
Trevor Robinson
1

Некоторое время назад я столкнулся с тем же самым, действительно, обещание может быть выполнено только один раз, другие попытки ничего не сделают (без ошибок, без предупреждений, без thenвызова).

Я решил обойти это так:

getUsers(users => showThem(users));

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

просто передайте свою функцию как обратный вызов и вызывайте ее столько раз, сколько захотите! Надеюсь, это имеет смысл.

Дамиано
источник
Я считаю это неправильным. Вы можете просто вернуть обещание getUsersи затем вызывать его .then()столько раз, сколько захотите. Нет необходимости передавать обратный вызов. На мой взгляд, одним из преимуществ обещаний является то, что вам не нужно указывать обратный вызов заранее.
Джон Хенкель
@JohnHenckel Идея состоит в том, чтобы выполнять обещание несколько раз, т.е. возвращать данные несколько раз, а не иметь несколько .thenоператоров. Как бы то ни было, я думаю, что единственный способ возвращать данные несколько раз в вызывающий контекст - это использовать обратные вызовы, а не обещания, поскольку обещания не были созданы для работы таким образом.
T. Rex
0

Если вам нужно изменить возвращаемое значение обещания, просто верните новое значение в thenцепочке далее then/ catchна нем.

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
источник
0

Нет четкого способа выполнить обещания несколько раз, потому что, поскольку оно решено, оно выполнено. Лучшим подходом здесь является использование шаблона, наблюдаемого наблюдателем, например, я написал следующий код, который отслеживает событие клиента сокета. Вы можете расширить этот код, чтобы удовлетворить ваши потребности

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

Вы можете написать тесты, чтобы подтвердить поведение.

Выполнив следующий тест, вы можете сделать вывод, что

Вызов resolve () / reject () никогда не вызывает ошибки.

После установки (отклонения) разрешенное значение (отклоненная ошибка) будет сохраняться независимо от следующих вызовов resolve () или reject ().

Вы также можете проверить мою запись в блоге для подробностей.

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

})
трансанг
источник
-1

Что вам нужно сделать, так это поставить ng-if на свой основной ng-выход и вместо этого показать загрузочный счетчик. Как только ваш языковой стандарт загружен, вы показываете розетку и позволяете отображать иерархию компонентов. Таким образом, все ваше приложение может предполагать, что языковой стандарт загружен и никаких проверок не требуется.

Адриан Брэнд
источник