扩展 Javascript 承诺并在构造函数中解决或拒绝它

Posted

技术标签:

【中文标题】扩展 Javascript 承诺并在构造函数中解决或拒绝它【英文标题】:Extend Javascript promise and resolve or reject it inside constructor 【发布时间】:2018-06-17 22:18:51 【问题描述】:

我想用 ES6 语法扩展原生 javascript Promise 类,并且能够在子类构造函数中调用一些异步函数。根据异步函数结果,promise 必须被拒绝或解决。

然而,当then函数被调用时,会发生两件奇怪的事情:

    子类构造函数执行两次 “Uncaught TypeError: Promise resolve or reject function is not callable”错误抛出

    class MyPromise extends Promise 
        constructor(name) 
            super((resolve, reject) => 
                setTimeout(() => 
                    resolve(1)
                , 1000)
            )

            this.name = name
        
    

    new MyPromise('p1')
        .then(result => 
            console.log('resolved, result: ', result)
        )
        .catch(err => 
            console.error('err: ', err)
        )

【问题讨论】:

github.com/nodejs/node/issues/13678#issuecomment-326812117 【参考方案1】:

您必须通过实现then 方法使其thenable

否则,将调用超类 Promise 的超类,并尝试使用您的 MyPromise' 构造函数创建另一个 Promise,这与原始 Promise 构造函数不兼容。

问题是,正确实现 then 方法很棘手,就像 Promise 一样。您最终可能会将 Promise 的实例作为成员,而不是作为超类。

【讨论】:

【参考方案2】:

推理很简单,但不一定是不言而喻的。

.then() 返回一个承诺 如果在 Promise 的子类上调用 then,则返回的 Promise 是子类的实例,而不是 Promise 本身。 then 返回的 Promise 是通过调用子类构造函数构造的,并向其传递一个内部执行函数,该函数记录传递给它的 resolvereject 参数的值以供以后使用。 “稍后使用”包括在监视 onfulfilledonrejected 处理程序(稍后)的执行以查看它们是否返回值(这解决了 then 返回的承诺时)异步解决或拒绝 then 返回的承诺) 或抛出错误(拒绝承诺)。

简而言之,then 调用在内部获取并记录对它们返回的 Promise 的 resolvereject 函数的引用。


所以关于这个问题,
new MyPromise( 'p1')

工作正常,是对子类构造函数的第一次调用。

.then( someFunction)

someFunction 记录在对new MyPromise 进行的then 调用列表中(调用then 可以多次调用)并尝试通过调用来创建返回承诺

new MyPromise( (resolve, reject) => ... /* store resolve reject references */

这是来自then 代码的对子类构造函数的第二次调用。构造函数应该(并且确实)同步返回。

在从创建返回的承诺返回时,.then 方法会进行完整性检查,以查看它需要供以后使用的 resolvereject 函数是否实际上是函数。它们应该与then 调用中提供的回调一起存储(在列表中)。

MyPromise 的情况下,它们不是。通过then 传递给MyPromise 的执行程序甚至没有被调用。所以then 方法代码会抛出一个类型错误“Promise resolve or reject function is not callable”——它没有办法解决或拒绝它应该返回的 Promise。

在创建 Promise 的子类时,子类构造函数必须将执行器函数作为其第一个参数,并使用真正的 resolvereject 函数参数调用执行器。这是then 方法代码内部需要的。

使用MyPromise 做一些复杂的事情,也许检查第一个参数以查看它是否是一个函数,如果是,则将其作为执行程序调用,可能是可行的,但超出了此答案的范围!对于显示的代码,编写工厂/库函数可能更简单:

function namedDelay(name, delay=1000, value=1) 
     var promise = new Promise( (resolve,reject) => 
         setTimeout(() => 
                resolve(value)
            , delay)
         
     );
    promise.name = name;
    return promise;


namedDelay( 'p1')
    .then(result => 
        console.log('fulfilled, result: ', result)
    )
    .catch(err => 
        console.error('err: ', err)
    )

;TLDR

Promise 的类扩展不是扩展。如果是,则需要实现 Promise 接口并将执行器函数作为第一个参数。您可以使用工厂函数返回异步解析的 Promise(如上),或者使用 hack 发布的代码

MyPromise.prototype.constructor = Promise

这会导致.then 返回一个常规的 Promise 对象。 hack 本身驳斥了正在发生类扩展的想法。


Promise 扩展示例

以下示例显示了一个基本的 Promise 扩展,它添加了提供给构造函数的属性。注意:

Symbol.toString getter 仅影响将实例转换为字符串的输出。在测试的浏览器控制台上记录实例 object 时,它不会将“Promise”更改为“MyPromise”。

Firefox 89 (Proton) 没有报告扩展实例的自身属性,而 Chrome 报告了 - 下面的测试代码按名称记录实例属性的原因。

class MyPromise extends Promise 
    constructor(exec, props) 
        if( typeof exec != "function") 
            throw TypeError( "new MyPromise(executor, props): an executor function is required");
        
        super((resolve, reject) => exec(resolve,reject));
        if( props) 
            Object.assign( this, props);
         
    
    get [Symbol.toStringTag]() 
        return 'MyPromise';
    


// Test the extension:
const p1 = new MyPromise( (resolve, reject) =>
    resolve(42),
    id: "p1", bark: ()=>console.log("woof") );

console.log( "p1 is a %s object", p1.constructor.name);
console.log( "p1.toString() = %s", p1.toString());
console.log( "p1.id = '%s'", p1.id);
console.log( "p1 says:"); p1.bark();

const pThen = p1.then(data=>data);
console.log( "p1.then() returns a %s object", pThen.constructor.name);
let pAll = MyPromise.all([Promise.resolve(39)]);
console.log( "MyPromise.all returns a %s object", pAll.constructor.name);
try  new MyPromise(); 
catch(err) 
    console.log( "new MyPromise() threw: '%s'", err.message);

【讨论】:

感谢@traktor53 提供完整的逻辑描述。我猜像jsfiddle.net/p7b6gaqd/15 这样的东西应该也能正常工作? @Soul_man 代码似乎正朝着正确的方向发展,但如前所述,“超出了此答案的范围”。鉴于 cmets 不是扩展现有问题的地方,如果您需要更多帮助和/或反馈,请在此处或 Code Review 提出新问题。它也让其他人有机会回答:-) 所以,因为 MyPromise 的构造函数,而不是 Promise 的构造函数,用于构造 派生 Promises,就像 Promise 的构造函数一样可以,你必须运行给定的执行程序(如果有的话),并在MyPromise 的构造函数中正确地提供你从超类Promise 获得的resolvereject 函数。好的,我想我明白了。【参考方案3】:

asdru 的帖子包含正确答案,但也包含应不鼓励的方法(构造函数破解)。

构造函数 hack 检查构造函数参数是否为函数。这不是要走的路,因为 ECMAScript 设计包含通过 Symbol.species 对 Promises 进行子类化的特定机制。

asdru 对使用Symbol.species 的评论是正确的。见当前ECMAScript specification中的解释:

Promise 原型方法通常使用其 this 值的构造函数 创建派生对象。但是,子类构造函数可能 通过重新定义其 @@species 属性来覆盖该默认行为。

规范(间接)在finallythen 部分中引用了此注释(查找提及SpeciesConstructor)。

通过返回Promise 作为物种构造函数,避免了traktor 的答案分析得如此清楚的问题。 then 调用 Promise 构造函数,但不调用子类 MyPromise 构造函数。 MyPromise 构造函数只使用name 参数调用一次,不需要或不适当的进一步的参数检查逻辑。

因此,代码应该是:

class MyPromise extends Promise 
    constructor(name) 
        super((resolve, reject) => 
            setTimeout(() => 
                resolve(1)
            , 1000)
        )
        this.name = name
    

    static get [Symbol.species]() 
        return Promise;
    

    get [Symbol.toStringTag]() 
        return 'MyPromise';
    

少即是多!

一些注意事项:

MDN 有一个使用物种符号扩展Array 的示例。

最新的浏览器版本(Chrome、FF、Safari、Mac 和 Linux 上的 Edge)可以正确处理此问题,但我没有关于其他浏览器或旧版本的信息。

Symbol.toStringTag 是一个非常好的触摸,但不是必需的。大多数浏览器使用此符号返回的值来识别控制台中的子类承诺,但请注意,FF 不会 - 这很容易造成混淆。然而,在所有浏览器中,new MyPromise('mine').toString() 产生 "[object MyPromise]"

如果您在 Typescript 中创作,所有这些也都没有问题。

正如noseratio 指出的那样,扩展 Promises 的主要用例是包装支持中止或取消逻辑(FileReader、fetch 等)的(旧版)API。

【讨论】:

但是如果你不保持与Promise构造函数的兼容性,你将无法使用MyPromise.raceMyPromise.all,违反了LSP SOLID原则。对于Symbol.toStringTag,是的,没什么用,我添加它只是为了完整性 Symbol.species getter 返回Promise 会导致调用MyPromise 对象的then 方法返回一个Promise 对象而不是MyPromise 对象,这使得扩展最多是部分的。如果省略 Symbol.species getter,调用继承的 Mypromise 对象的 then 方法会引发错误,因为“扩展”类构造函数不支持执行器函数(如帖子中所述)。【参考方案4】:

我发现延长承诺的最佳方式是

class MyPromise extends Promise 
    constructor(name) 
        // needed for MyPromise.race/all ecc
        if(name instanceof Function)
            return super(name)
        
        super((resolve, reject) => 
            setTimeout(() => 
                resolve(1)
            , 1000)
        )

        this.name = name
    

    // you can also use Symbol.species in order to
    // return a Promise for then/catch/finally
    static get [Symbol.species]() 
        return Promise;
    

    // Promise overrides his Symbol.toStringTag
    get [Symbol.toStringTag]() 
        return 'MyPromise';
    



new MyPromise('p1')
    .then(result => 
        console.log('resolved, result: ', result)
    )
    .catch(err => 
        console.error('err: ', err)
    )

【讨论】:

我对@9​​87654321@ 使用了类似的方法,但我不知道[theSymbol.species] 的技巧,谢谢! 另外:Constructor of a custom promise class is called twice (extending standard Promise).

以上是关于扩展 Javascript 承诺并在构造函数中解决或拒绝它的主要内容,如果未能解决你的问题,请参考以下文章

ERROR 错误:未捕获(承诺中):TypeError:i.BehaviorSubject 不是 Angular 10 s-s-r 中的构造函数

将变量保存在具有承诺的函数中的Javascript问题[重复]

拒绝承诺时如何解决 UnhandledPromiseRejectionWarning

处理类构造函数中的承诺错误[重复]

扩展窗口类

javascript 扩展构造函数以接收参数