扩展 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
方法使其then
able。
否则,将调用超类 Promise
的超类,并尝试使用您的 MyPromise
' 构造函数创建另一个 Promise
,这与原始 Promise
构造函数不兼容。
问题是,正确实现 then
方法很棘手,就像 Promise
一样。您最终可能会将 Promise
的实例作为成员,而不是作为超类。
【讨论】:
【参考方案2】:推理很简单,但不一定是不言而喻的。
.then()
返回一个承诺
如果在 Promise 的子类上调用 then
,则返回的 Promise 是子类的实例,而不是 Promise 本身。
then
返回的 Promise 是通过调用子类构造函数构造的,并向其传递一个内部执行函数,该函数记录传递给它的 resolve
和 reject
参数的值以供以后使用。
“稍后使用”包括在监视 onfulfilled
或 onrejected
处理程序(稍后)的执行以查看它们是否返回值(这解决了 then
返回的承诺时)异步解决或拒绝 then
返回的承诺) 或抛出错误(拒绝承诺)。
简而言之,then
调用在内部获取并记录对它们返回的 Promise 的 resolve
和 reject
函数的引用。
所以关于这个问题,
new MyPromise( 'p1')
工作正常,是对子类构造函数的第一次调用。
.then( someFunction)
将someFunction
记录在对new MyPromise
进行的then
调用列表中(调用then
可以多次调用)并尝试通过调用来创建返回承诺
new MyPromise( (resolve, reject) => ... /* store resolve reject references */
这是来自then
代码的对子类构造函数的第二次调用。构造函数应该(并且确实)同步返回。
在从创建返回的承诺返回时,.then
方法会进行完整性检查,以查看它需要供以后使用的 resolve
和 reject
函数是否实际上是函数。它们应该与then
调用中提供的回调一起存储(在列表中)。
在MyPromise
的情况下,它们不是。通过then
传递给MyPromise
的执行程序甚至没有被调用。所以then
方法代码会抛出一个类型错误“Promise resolve or reject function is not callable”——它没有办法解决或拒绝它应该返回的 Promise。
在创建 Promise 的子类时,子类构造函数必须将执行器函数作为其第一个参数,并使用真正的 resolve
和 reject
函数参数调用执行器。这是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
的构造函数,用于构造 派生 Promise
s,就像 Promise
的构造函数一样可以,你必须运行给定的执行程序(如果有的话),并在MyPromise
的构造函数中正确地提供你从超类Promise
获得的resolve
和reject
函数。好的,我想我明白了。【参考方案3】:
asdru
的帖子包含正确答案,但也包含应不鼓励的方法(构造函数破解)。
构造函数 hack 检查构造函数参数是否为函数。这不是要走的路,因为 ECMAScript 设计包含通过 Symbol.species
对 Promises 进行子类化的特定机制。
asdru
对使用Symbol.species
的评论是正确的。见当前ECMAScript specification中的解释:
Promise 原型方法通常使用其 this 值的构造函数 创建派生对象。但是,子类构造函数可能 通过重新定义其 @@species 属性来覆盖该默认行为。
规范(间接)在finally
和then
部分中引用了此注释(查找提及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.race
和MyPromise.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)
)
【讨论】:
我对@987654321@ 使用了类似的方法,但我不知道[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问题[重复]