【问题标题】:Possible to add a cancel method to Promise in Typescript?可以在 Typescript 中向 Promise 添加取消方法吗?
【发布时间】:2017-09-28 05:47:32
【问题描述】:

我想创建一个 QueryPromise,它只是一个带有取消方法的具体承诺。以下是它的使用方法:

function runQuery(text: string): QueryPromise {
  return new QueryPromise((resolve,reject) => {nativeQuery(resolve)})
}

这是我的第一次尝试,希望很容易理解:

interface CancellablePromise<T> extends Promise<T> {
  cancel: () => void
}

// create the promise my app will use
type QueryPromise = CancellablePromise<string|boolean>

但这还不够。

经过几个小时的反复试验,我设法接近了,但这似乎很乏味,而且绝非干燥。

interface CancellablePromise<T> extends Promise<T> {
  cancel: () => void
}
// also need this interface so the variable can be declared
// plus need to redeclare methods to return the new type
interface CancellablePromiseConstructor extends PromiseConstructor {
  new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): CancellablePromise<T>;
  cancel: () => void
}

type QueryPromise = CancellablePromise<string|boolean>    // for parameter types
var QueryPromise: CancellablePromiseConstructor = Promise // for the new operator

// some example code to exercise the promise
function runQuery(text: string): QueryPromise {
  return new QueryPromise((resolve,reject) => {nativeQuery(resolve)})
}

我觉得我走错了路……有谁知道更好的方法吗?

【问题讨论】:

  • QueryPromise 是否在其他地方定义?什么增加了取消方法?创建接口不是因为它们只表示类型而不是值。
  • 我的 2 美分……只需使用一个 observable,关闭流将被视为取消。这里的订阅签名:$anObservable.subscribe(/onValue/,/onError/,/onClose/);
  • @AluanHaddad 这是一个很好的观点。现在,我的 runQuery 函数将 cancel 方法添加到了 Promise 中。我在这个例子中省略了它,因为这篇文章已经很长了,一旦这个代码的结构被固定,那部分应该是显而易见的。

标签: typescript


【解决方案1】:

TypeScript interfacestypes 描述合约。你的很好:

interface CancellablePromise<T> extends Promise<T> {
  cancel: () => void
}

type QueryPromise = CancellablePromise<string | boolean>

然后您可以根据需要执行合同。这是一个例子:

function runQuery(text: string): QueryPromise {
  let rejectCb: (err: Error) => void
  let p: Partial<QueryPromise> = new Promise<string | boolean>((resolve, reject) => {
    rejectCb = reject
    /* ... Here the implementation of the query ... */
  });
  p.cancel = () => {
    /* ... Here the implementation of the aborting ... */
    rejectCb(new Error("Canceled"))
  }
  return p as QueryPromise
}

注意事项:

  • cancel 的实现应该拒绝承诺;
  • 我使用Partial&lt;QueryPromise&gt; 以便之后添加成员cancel

【讨论】:

  • 简单而且效果很好。谢谢你。我没有使用 Partial,而是在 new 语句中执行类型转换:let p = new Promise&lt;string | boolean&gt;((resolve, reject) =&gt; {...}) as QueryPromise;
  • 太糟糕了,我无法在我的 executor 函数中访问 Promise ......摆脱 rejectCb 将是一个很好的代码简化。
【解决方案2】:

可以创建一个继承自 Promise 的类,并在 Promise 构造函数的 executor 参数中添加 OnCancel:

export class CancellablePromise<T> extends Promise<T> {
    private onCancel: () => void

    constructor(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void, onCancel: (cancelHandler: () => void) => void) => void) {
        let onCancel: () => void;
        super((rs, rj) => executor(rs, rj, (ch: () => void) => onCancel = ch));
        this.onCancel = onCancel;
    }

    public cancel() {
        if (this.onCancel) {
            this.onCancel();
        }
    }
}

这是一个使用示例:

public static search(query: string): CancellablePromise<Region[]> {
    return new CancellablePromise((resolve, reject, onCancel) => {
        const request = $.ajax({
            type: "GET",
            url: ROOT_URL + 'api/Regions',
            data: { q: query },
            success: (regions) => resolve(regions),
            error: (jqXHR) => reject(jqXHR.responseText),
        });
        onCancel(() => {
            if (request.state() == "pending") request.abort();
        });
    });
}

如您所见,这个实现可以像任何其他 Promise 一样构建。它可以用于任何 Promise 可以使用的地方,并且它包含一个取消方法。

这是一个取消承诺的例子:

// Cancel previous search (if any)
if (this.currentSearch) {
    this.currentSearch.cancel();
}

this.currentSearch = Regions.search(query);
this.currentSearch
    .then((regions) => {
        if (regions) {
            this.setState({ isSearching: false, regions: regions });
        } else {
            this.setState({ isSearching: false, regions: [] });
        }
    })
    .catch(() => this.setState({ isSearching: false, regions: [] }));

【讨论】:

    【解决方案3】:

    几个月前我实现了类似的东西。我的解决方案与您所需的 API 不完全匹配,但它可能对您有用,或者您可以根据自己的需要进行调整:

    取消错误

    首先你应该知道,有一个没有解决也没有拒绝的承诺是一个坏主意。第三种状态(已取消)将破坏现有代码,并且未更改为显式查找取消的应用程序将永远挂起。这就是为什么我在取消承诺时以特殊的Canceled 错误拒绝承诺的原因。取消感知拒绝处理程序可以通过查看错误类型(使用instanceof)来区分失败和取消。不知道取消的拒绝处理程序会将取消视为错误。

    下面是这样一个自定义错误类的简单实现:

    class Canceled extends Error {
        constructor(reason: string = "") {
            super(reason);
            Object.setPrototypeOf(this, Canceled.prototype);
        }
    }
    

    需要setPrototypeOf 调用,因此instanceof 可用于检测此错误类型的实例。

    可取消界面

    接下来,您需要一个可取消承诺的接口。我的和你的差不多:

    interface Cancelable<T> extends Promise<T> {
        cancel(reason?: string): Cancelable<T>;
    }
    

    可取消功能

    创建 Cancelable 接口的实现类是个坏主意。很难从内置的 Promise 类型继承。所以我决定使用标准的 Promise 对象,并简单地将 cancel 方法添加到 Promise 实例中,而不是创建一个新类型。为此,我使用此功能:

    function cancelable<T>(promise: Promise<T>, onCancel?: (canceled: Canceled) => void): Cancelable<T> {
        let cancel: ((reason: string) => Cancelable<T>) | null = null;
        let cancelable: Cancelable<T>;
        cancelable = <Cancelable<T>>new Promise((resolve, reject) => {
            cancel = (reason: string = "") => {
                try {
                    if (onCancel) {
                        onCancel(new Canceled(reason));
                    }
                } catch (e) {
                    reject(e);
                }
                return cancelable;
            };
            promise.then(resolve, reject);
        });
        if (cancel) {
            cancelable.cancel = cancel;
        }
        return cancelable;
    }
    

    所以这个函数将一个普通的 Promise 作为第一个参数,将一个 onCancel 回调作为第二个参数,当在返回的 Cancelable 上调用 cancel() 方法时调用它。

    创建可取消的承诺

    要真正创建一个可取消的承诺,你必须用cancelable 函数包装一个标准的承诺和你的取消处理程序。这是一个示例睡眠函数,它在给定时间后解决(使用setTimeout)并且可以取消(使用clearTimeout 清除超时):

    function sleep(ms: number): Cancelable<void> {
        let timer: any;
        return cancelable(new Promise(resolve => {
            timer = setTimeout(resolve, ms);
        }), canceled => {
            clearTimeout(timer);
            throw canceled;
        });
    }
    

    请注意,onCancel 回调接收到 canceled 参数,它是 Canceled 错误类型的实例。当 promise 被成功取消时,onCancel 回调必须抛出这个错误。如果由于某种原因无法取消承诺,那么只需不要抛出错误,承诺将继续其正常工作。

    如何取消承诺

    最后,这里有一个示例如何取消可取消的承诺以及如何对其做出反应:

    const waiting = sleep(1000);
    waiting.then(() => {
        console.log("Wait successful");
    }, error => {
        if (error instanceof Canceled) {
            console.log("Wait canceled. Reason: " + error.message);
        } else {
            console.error("Wait failed:", error);
        }
    });
    
    waiting.cancel("Nah, stop waiting");
    

    我希望这对您有用,或者至少可以为您提供一些如何进行自己的实现的想法。

    【讨论】:

      【解决方案4】:

      @kayahr 的解决方案非常聪明和优雅。但我不会使用,因为在 Promise 的执行器中中断线程是非常粗鲁的。

      谁知道呢,也许有一些 IO 操作。读/写操作。而这样的取消可能是危险的。我的意思是文件也不能关闭或类似的东西。

      在我的工作中,我还需要一个“可取消”的承诺。我做了一点其他的解决方案。

      这个想法是,它并没有真正取消,这是一种获得更多结果选择的方法。实际上,promise 给了我们 2 + 1 种方式:

      1. 然后:全部完成,一切正常
      2. 捕捉:哦,错误
      3. 终于:做吧,做吧

      但是如果出于某种原因我们想停止运营,我们没有这样的选择。

      有了这个解决方案,这是可能的。

      export type TResolver<T> = (value?: T) => void;
      export type TRejector = (error: Error) => void;
      export type TFinally = () => void;
      export type TCanceler<T> = (reason?: T) => void;
      
      export class CancelablePromise<T, C> {
      
          private _resolvers: Array<TResolver<T>> = [];
          private _rejectors: TRejector[] = [];
          private _cancelers: Array<TCanceler<C>> = [];
          private _finishes: TFinally[] = [];
          private _canceled: boolean = false;
          private _resolved: boolean = false;
          private _rejected: boolean = false;
          private _finished: boolean = false;
      
          constructor(
              executor: (resolve: TResolver<T>, reject: TRejector, cancel: TCanceler<C>, self: CancelablePromise<T, C>) => void,
          ) {
              const self = this;
              // Create and execute native promise
              new Promise<T>((resolve: TResolver<T>, reject: TRejector) => {
                  executor(resolve, reject, this._doCancel.bind(this), self);
              }).then((value: T) => {
                  this._doResolve(value);
              }).catch((error: Error) => {
                  this._doReject(error);
              });
          }
      
          public then(callback: TResolver<T>): CancelablePromise<T, C> {
              this._resolvers.push(callback);
              return this;
          }
      
          public catch(callback: TRejector): CancelablePromise<T, C> {
              this._rejectors.push(callback);
              return this;
          }
      
          public finally(callback: TFinally): CancelablePromise<T, C> {
              this._finishes.push(callback);
              return this;
          }
      
          public cancel(callback: TCanceler<C>): CancelablePromise<T, C> {
              this._cancelers.push(callback);
              return this;
          }
      
          public break(reason: C): CancelablePromise<T, C> {
              this._doCancel(reason);
              return this;
          }
      
          private _doResolve(value: T) {
              if (this._canceled) {
                  return;
              }
              this._resolved = true;
              this._resolvers.forEach((resolver: TResolver<T>) => {
                  resolver(value);
              });
              this._doFinally();
          }
      
          private _doReject(error: Error) {
              if (this._canceled) {
                  return;
              }
              this._rejected = true;
              this._rejectors.forEach((rejector: TRejector) => {
                  rejector(error);
              });
              this._doFinally();
          }
      
          private _doFinally() {
              if (this._finished) {
                  return;
              }
              this._finished = true;
              this._finishes.forEach((handler: TFinally) => {
                  handler();
              });
          }
      
          private _doCancel(reason?: C) {
              if (this._resolved || this._rejected || this._canceled) {
                  // Doesn't make sence to cancel, because it was resolved or rejected or canceled already
                  return this;
              }
              this._canceled = true;
              this._cancelers.forEach((cancler: TCanceler<C>) => {
                  cancler(reason);
              });
              this._doFinally();
          }
      
      }
      

      让我们看看它是如何工作的。

      const a: CancelablePromise<void, void> = new CancelablePromise<void, void>((resolve, reject, cancel) => {
          // Do some long operation
          setTimeout(() => {
              resolve();
          }, 1000);
      }).then(() => {
          console.log('resolved');
      }).finally(() => {
          console.log('finally done');
      });
      

      很像正常的承诺。在输出中:

      resolved
      finally done
      

      让我们尝试拒绝:

      const b: CancelablePromise<void, void> = new CancelablePromise<void, void>((resolve, reject, cancel) => {
          // Do some long operation
          setTimeout(() => {
              console.log('timer #1');
              resolve();
          }, 1000);
          setTimeout(() => {
              console.log('timer #2');
              reject(new Error('Because I can!'));
          }, 500);
      }).then(() => {
          console.log('resolved');
      }).catch((error: Error) => {
          console.log('error');
      }).finally(() => {
          console.log('finally done');
      });
      

      输出:

      timer #2
      error
      finally done
      timer #1
      

      现在我们将取消它

      const c: CancelablePromise<void, void> = new CancelablePromise<void, void>((resolve, reject, cancel) => {
          // Do some long operation
          setTimeout(() => {
              console.log('timer #1');
              resolve();
          }, 1000);
          setTimeout(() => {
              console.log('timer #2');
              reject(new Error('Because I can!'));
          }, 500);
          setTimeout(() => {
              console.log('timer #3');
              cancel();
          }, 250);
      }).then(() => {
          console.log('resolved');
      }).cancel(() => {
          console.log('canceled');
      }).catch((error: Error) => {
          console.log('error');
      });
      

      输出

      timer #3
      timer #2
      canceled
      timer #1
      

      很好,我们没有触发 resolve 或 reject,但是(!)我们的执行器在 promise 被取消后仍然工作。这不好,这里有一种方法可以修复它并以真正的方式取消它。

      const d: CancelablePromise<void, void> = new CancelablePromise<void, void>((resolve, reject, cancel, self) => {
          // Listen cancel inside executer to cancel all correctly
          self.cancel(() => {
              clearTimeout(t1);
              clearTimeout(t2);
          });
          // Do some long operation
          const t1: any = setTimeout(() => {
              console.log('timer #1');
              resolve();
          }, 1000);
          const t2: any = setTimeout(() => {
              console.log('timer #2');
              reject(new Error('Because I can!'));
          }, 500);
          setTimeout(() => {
              console.log('timer #3');
              cancel();
          }, 250);
      }).then(() => {
          console.log('resolved');
      }).cancel(() => {
          console.log('canceled');
      }).catch((error: Error) => {
          console.log('error');
      });
      

      输出

      timer #3
      canceled
      

      因此,计时器 #1 和 #2 已停止 - 承诺被小心取消。

      当然我们也可以在外面取消。

      const d: CancelablePromise<void, void> = new CancelablePromise<void, void>((resolve, reject, cancel, self) => {
          // Listen cancel inside executer to cancel all correctly
          self.cancel(() => {
              clearTimeout(t1);
              clearTimeout(t2);
          });
          // Do some long operation
          const t1: any = setTimeout(() => {
              console.log('timer #1');
              resolve();
          }, 1000);
          const t2: any = setTimeout(() => {
              console.log('timer #2');
              reject(new Error('Because I can!'));
          }, 500);
      }).then(() => {
          console.log('resolved');
      }).cancel(() => {
          console.log('canceled');
      }).catch((error: Error) => {
          console.log('error');
      });
      
      setTimeout(() => {
          console.log('timer #3 (outside)');
          d.break();
      }, 250);
      

      结果会一样:

      timer #3 (outside)
      canceled
      

      希望它有用。

      【讨论】:

        【解决方案5】:

        这个实现怎么样?

        declare class CancelablePromise<T> extends Promise<T> {
          declare readonly cancel: () => void
        }
        
        interface Promise<T> {
          asCancelable(): CancelablePromise<T>
        }
        
        
        Promise.prototype.asCancelable = function () {
          let cancel: (reason: {cancelled: boolean}) => void
          const wrappedPromise = new Promise((resolve, reject) => {
            cancel = reject
            Promise.resolve(this)
              .then(resolve)
              .catch(reject)
          }) as any
          wrappedPromise.cancel = () => {
            cancel({cancelled: true})
          }
          return wrappedPromise as CancelablePromise<unknown>
        }
        

        用法:

        const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
        const waiting = sleep(1000).asCancelable()
        waiting.then(() => {
          console.info('this never get called')
        })
        waiting.cancel()
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2011-06-02
          • 1970-01-01
          • 2015-11-29
          • 1970-01-01
          • 2010-09-10
          • 1970-01-01
          • 2019-06-27
          相关资源
          最近更新 更多