約束を複数回解決しても安全ですか?


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て、約束が解決されるのを待つというものです。しかし、この関数の目的はロケールがロードされること確認することだけであることを考えると、ユーザーがそれを数回呼び出すことは完全に問題ありません。

私は現在、1つのpromiseを格納し、ロケールがサーバーから正常に取得された後にユーザーが関数を再度呼び出した場合にそれを解決しています。

私の知る限りでは、これは意図したとおりに機能していますが、これが適切なアプローチであるかどうか疑問に思っています。


7
この回答を参照してください。
robertklep 2013

私もそれを使用したことがあり、問題なく動作します。
Chandermani 2013

回答:


119

私は現在の約束を理解しているように、これは100%罰金になるはずです。理解しておくべきことは、一度解決(または拒否)されると、それは据え置きオブジェクトの場合であり、それが行われることです。

then(...)もう一度promise を呼び出す必要がある場合は、すぐに(最初の)解決/拒否された結果を取得する必要があります。

への追加の呼び出しはresolve()効果がありません(すべきではありませんか?)。reject以前に延期されていたオブジェクトを試みた場合、どうなるかわかりresolvedません(私は何も疑っていません)。


28
上記のすべてが実際に当てはまることを示すJSBinは次のとおりです。jsbin.com / gemepay/3 / edit?js , console最初の解決のみが使用されます。
konrad 2017

4
これに関する公式のドキュメントを見つけた人はいますか?それが現在機能している場合でも、文書化されていない動作に依存することは一般に推奨されません。
3ocene

ecma-international.org/ecma-262/6.0/#sec-promise.resolve-私はこれまで本質的にUNSAFEであると述べているものは何も発見されていません。ハンドラーが実際に1回だけ実行する必要があることを行う場合、アクションを再度実行する前に、ハンドラーでいくつかの状態を確認および更新します。しかし、私はまた、いくつかの公式のMDNエントリまたは仕様ドキュメントを完全に明確にしてほしいと思います。
demaniak 2018

PromiseA +ページに「問題」が何も表示されません。promisesaplus.comを
demaniak

3
@demaniakこの質問は、Promises / A +に関するものであり、ES6 に関するものではありません。しかし、あなたの質問に答えるために、無関係な解決/拒否が安全であるというES6仕様の一部がここにあります
Trevor Robinson

1

私は少し前に同じことに直面しました、確かに約束は一度しか解決できません、別の試みは何もしません(エラーなし、警告なし、then呼び出しなし)。

私はそれをこのように回避することに決めました:

getUsers(users => showThem(users));

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

関数をコールバックとして渡して、何度でも呼び出します!それが理にかなっていると思います。


これは間違っていると思います。あなたは単に約束を返し、getUsersそれから.then()あなたが望むだけ何度でもその約束を呼び出すことができます。コールバックを渡す必要はありません。私の見解では、promiseの利点の1つは、コールバックを事前に指定する必要がないことです。
John Henckel、

@JohnHenckelアイデアは、約束を複数回解決すること.thenです。つまり、複数のステートメントを持たず、データを複数回返します 。価値があるのは、呼び出しコンテキストにデータを複数回返す唯一の方法は、Promiseがそのように機能するように構築されていないため、Promiseではなくコールバックを使用することだと思います。
T.レックス

0

promiseの戻り値を変更する必要がある場合は、単に新しい値を返し、thennext 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);
});


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-outletに置き、代わりにローディングスピナーを表示することです。ロケールが読み込まれると、アウトレットが表示され、コンポーネント階層がレンダリングされます。このように、すべてのアプリケーションは、ロケールがロードされていることを想定でき、チェックは必要ありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.