重复解决一个Promise是否安全?

172

我在我的应用程序中有一个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并等待承诺被解决。但是,考虑到该函数的目的仅是确保区域设置已加载,如果用户多次调用它,也完全可以。

我目前只存储一个承诺,如果用户在从服务器成功获取区域设置后再次调用该函数,则解决它。

据我所知,这样做是按预期工作的,但我想知道这是否是适当的方法。


7
请查看此答案 - robertklep
我也使用过它,效果很好。 - Chandermani
8个回答

211

据我目前的理解,这完全没问题。唯一需要了解的是,一旦一个延迟对象被解决(或拒绝),它就完成了。

如果您再次在其承诺上调用then(...),您将立即获得(第一个)已解决/已拒绝的结果。

resolve()的其他调用将不会产生任何效果。

下面是一个可执行片段,涵盖了这些用例:

var p = new Promise((resolve, reject) => {
  resolve(1);
  reject(2);
  resolve(3);
});

p.then(x => console.log('resolved to ' + x))
 .catch(x => console.log('never called ' + x));

p.then(x => console.log('one more ' + x));
p.then(x => console.log('two more ' + x));
p.then(x => console.log('three more ' + x));


37
这里有一个 JSBin,说明上述所有内容都是真实的:http://jsbin.com/gemepay/3/edit?js,console 只有第一个 resolve 被使用。 - konrad
9
有人找到关于这个的官方文档吗?即使现在它能够正常工作,依赖未经记录的行为通常是不可取的。 - 3ocene
3
迄今为止,我尚未发现任何说明它本质上是不安全的东西。如果您的处理程序执行某些真正只应该执行一次的操作,我建议在再次执行该操作之前进行检查和更新某些状态。但我还想获取MDN官方条目或规范文档以获得绝对的清晰度。 - demaniak
我在 PromiseA+ 页面上没有发现任何“棘手”的问题。请参见 https://promisesaplus.com/。 - demaniak
1
你应该立即获取它,但不是立即/同步获取...它将在下一个事件循环的微任务队列中。 - Alexander Mills
8
@demaniak,这个问题是关于[Promises/A+](https://promisesaplus.com/)的,而不是ES6 promises。但是为了回答你的问题,关于ES6规范中关于多余的resolve/reject是安全的部分可以在[这里](https://www.ecma-international.org/ecma-262/6.0/#sec-promise-resolve-functions)找到。 - Trevor Robinson

1

我之前也遇到过同样的问题,事实上,一个 Promise 只能被解决一次,再次尝试不会有任何作用(没有错误、警告或 then 调用)。

我决定这样处理:

getUsers(users => showThem(users));

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

只需将您的函数作为回调传递,并根据需要多次调用它!希望这样说得清楚。


1
我认为这是错误的。你可以简单地从getUsers返回promise,然后在该promise上调用.then()多次。没有必要传递回调函数。在我看来,Promise的优点之一是您不需要事先指定回调函数。 - John Henckel
1
@JohnHenckel的意思是解析承诺多次,即多次返回数据,而不是有多个.then语句。说起来,我认为将数据多次返回到调用上下文的唯一方法是使用回调而不是承诺,因为承诺不是为以那种方式工作而构建的。 - SamAko
2
你可以将 .then(users => callback(users)) 简化为 .then(callback) - ErikE
没事了,ChatGPT解决了。这个网站无论如何都要关闭啦 xD - TheRealChx101
@TheRealChx101 祝你在ChatGPT上好运。希望它能如你所期望地发挥作用。 - ErikE
显示剩余2条评论

1

多次解决Promise的方法并不明确,因为一旦被解决,它就完成了。更好的方法是使用观察者-可观察者模式,例如我编写了以下代码来观察套接字客户端事件。您可以扩展此代码以满足您的需求。

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
如果您需要更改 Promise 的返回值,只需在 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);
});


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

})

-2

查看 Github Gist:reuse_promise.js

/*
reuse a promise for multiple resolve()s since promises only resolve once and then never again
*/

import React, { useEffect, useState } from 'react'

export default () => {
    
    const [somePromise, setSomePromise] = useState(promiseCreator())
        
    useEffect(() => {
        
        somePromise.then(data => {
            
            // do things here
            
            setSomePromise(promiseCreator())
        })
        
    }, [somePromise])
}

const promiseCreator = () => {
    return new Promise((resolve, reject) => {
        // do things
        resolve(/*data*/)
    })
}

-2
你应该在主要的ng-outlet上放置一个ng-if,并显示一个加载旋转器。一旦你的本地化被加载,你就可以显示出口并让组件层次结构渲染。这样,你的整个应用程序都可以假定本地化已经加载,不需要进行任何检查。

-3

不,多次解析/拒绝承诺是不安全的。这基本上是一个错误,很难捕捉到,因为它可能不总是可重现的。

有一种模式可以在调试时用于跟踪此类问题。关于这个主题的精彩讲座:Ruben Bridgewater — Error handling: doing it right!(与问题相关的部分约为40分钟)


2
为什么不安全?你错了。 - Elliott Beach

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接