可以在打字稿中添加取消方法以承诺



我想创建一个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)})
}

我觉得我走了很长一段路...有人知道这样做的更好方法吗?

TypeScript 接口类型描述合同。你的很好:

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<QueryPromise>以后添加成员cancel

可以创建一个从承诺继承的类,并在承诺构造函数的执行者参数中添加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();
        });
    });
}

您可以看到,可以像其他任何承诺一样构建此实现。它可以在承诺可以的任何地方使用,并且包括取消方法。

这是取消承诺的示例:

// 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: [] }));

我几个月前实现了类似的东西。我的解决方案与您所需的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>;
}

取消函数

创建可取消接口的实现类是一个坏主意。很难从内置的承诺类型继承。因此,我决定使用标准的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;
}

因此,此功能将正常的承诺作为第一个参数,而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错误类型的实例。当承诺成功取消时,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");

我希望这对您有效,或者至少给您一些想法如何进行自己的实施。

@kayahr的解决方案真的很聪明且优雅。但是我不会使用,因为将线程闯入Promise的执行者是非常无礼的。

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

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

这个想法是,这不是真正取消的,这是一种获得结果选择的方式。实际的承诺给我们2 1种方法:

  1. 然后:全部完成,全部确定
  2. catch :哦,错误
  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

很好,我们没有解决或拒绝解雇,但是(!)我们的执行者在取消承诺后仍在工作。这不好,这是一种修复并以真实方式取消的方法。

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

希望它将有用。

此实现如何?

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

最新更新