【问题标题】:How to implement promisified setTimeout with clearTimeout functionality?如何使用 clearTimeout 功能实现承诺的 setTimeout?
【发布时间】:2021-03-17 12:36:15
【问题描述】:

以下实现抛出错误(见下方评论),如何解决?

interface PromiseWithAbort extends Promise<unknown> {
  abort: () => void
}

export const pause = (
  ms?: number,
  cb?: (...args: unknown[]) => unknown,
  ...args: unknown[]
): PromiseWithAbort => {
  let timeout

  // Error: Property 'abort' is missing in type 'Promise<unknown>'
  // but required in type 'PromiseWithAbort'.
  const promise: PromiseWithAbort = new Promise((resolve, reject) => {
    timeout = setTimeout(async () => {
      try {
        resolve(await cb?.(...args))
      } catch (error) {
        reject(error)
      }
    }, ms)
  })

  promise.abort = () => clearTimeout(timeout)

  return promise
}

【问题讨论】:

  • 请注意,这种可取消承诺的简单方法无法扩展;一旦你在承诺上使用thencatch,你就会失去这个方法。这是一个棘手的问题,但您可以考虑将可选的AbortSignal 传递给pause
  • @T.J.Crowder 你能否详细说明“一旦你使用 then 或抓住承诺,你就失去了方法”?我真的不明白。
  • thencatch 等创建新的 Promise,通常与您调用它们的 Promise 类型相同。上面的 promise 是标准的 Promise,所以来自 then/catch/finally 的新 promise 不会有 abort 方法。
  • 再一次,我只是说它无法扩展。 Promise 的强大之处在于它们的标准语义以及将它们组合在一起的方式。当您像上面那样自定义实例时,您会破坏这两件事。那里的代码假定只有初始承诺的接收者有理由中止事情,但即使只是async function example() { return pause(); } example().abort(); 也会中断。无论如何,编码愉快!
  • :-) 我已经更新了答案。编码快乐!

标签: javascript typescript types type-definition


【解决方案1】:

问题是您分配给promise 的promise 没有abort 属性,但是您分配给promise 的类型需要它。解决此问题的一种简单方法是在将其分配给promise 之前添加它。 (这也可以让您摆脱 promise 上的显式类型。)

还有其他一些事情,请参阅*** cmets:

interface PromiseWithAbort extends Promise<unknown> {
    abort: () => void
}

export const pause = (
    ms?: number,
    cb?: (...args: unknown[]) => unknown,
    ...args: unknown[]
): PromiseWithAbort => {
    let timeout: number; // *** Need the type in order to avoid implicit `any`
  
    // *** Add `abort` to the promise before assigning to `promise`
    const promise = Object.assign(
        new Promise((resolve, reject) => {
            timeout = setTimeout(async () => {
                try {
                    resolve(await cb?.(...args));
                } catch (error) {
                    reject(error);
                }
             }, ms); // *** `ms` needs a default value, you're optionally passing `undefined`
        }), {
            abort: () => clearTimeout(timeout)
        }
    );
  
    return promise;
}

On the playground

也就是说,在cb(如果有的话)返回的promise上使用await并将结果传递给resolve有点迂回;相反,您可以将承诺传递给resolve,这会将您创建的承诺解析为cb(如果有)返回的承诺:

export const pause = (
    ms?: number,
    cb?: (...args: unknown[]) => unknown,
    ...args: unknown[]
): PromiseWithAbort => {
    let timeout: number;
  
    // *** Add `abort` to the promise before assigning to `promise`
    const promise = Object.assign(
        new Promise((resolve, reject) => {
            timeout = setTimeout(() => { // *** No need for `async`
                try {
                    resolve(cb?.(...args)); // *** No need for `await`, just resolve the promise to `cb`'s promise
                } catch (error) {
                    reject(error);
                }
             }, ms);
        }), {
            abort: () => clearTimeout(timeout)
        }
    );
  
    return promise;
}

On the playground


就其价值而言,我不会将abort 添加到承诺中,尤其是因为当您在该承诺上使用.then.catch 或在async 函数中使用它时,您的承诺从他们那里得到不会有abort 方法。相反,您可以考虑接受AbortSignal

我还会删除 cb 并将 pause 设为纯粹的暂停功能。 cb 不必要地复杂化;您可以使用.thenawait,然后在您的代码中直接调用cb

这是一个例子:

class CancelledError extends Error {
    constructor(msg = "Operation was cancelled") {
        super(msg);
    }
}

interface PauseOptions {
    signal?: AbortSignal;
    silent?: boolean;
}
export const pause = (
    ms: number,
    {signal, silent = false}: PauseOptions = {}
): Promise<void> => {
    return new Promise((resolve, reject) => {
        // Function we'll use if the operation is cancelled
        const cancelled = () => {
            if (!silent) {
                reject(new CancelledError());
            }
        };
        // The actual timer
        const handle = setTimeout(() => {
            if (signal?.aborted) { // It would be rare for this to happen
                cancelled();
            } else {
                resolve();
            }
        }, ms);
        // Handle cancellation
        signal?.addEventListener("abort", () => {
            clearTimeout(handle);
            cancelled();
        });
    });
};

On the playground

【讨论】:

  • 非常感谢您的出色回答!不过我还是有点疑惑,在上面的操场上,运行代码会记录caught,不就是处理cb返回的promise的拒绝吗?
  • @Leon - 我不能 100% 确定您所说的 " 是什么意思...在上面的操场上,运行代码会记录 caught...",但是您完全正确的是,您原来的try/catch 会被cb 的承诺拒绝,我在那里感到困惑。对于那个很抱歉!我已经确定了答案。也就是说,使用async 函数和await 没有必要,您只需将您的promise 解析为来自cb 的promise。 (无论如何,我已经在上面留下了try/catch - 因为我们不知道cb 是什么,所以我们不知道它是否会引发同步错误,返回一个它可能会拒绝的承诺等):-)
  • 抱歉描述模糊,我的意思是在包含Code链接的第一条评论中,运行代码将记录caught。再次感谢您的详细回复。
  • @WenfangDu - 哇,我……只是错过了那个链接,抱歉。 :-) 但下面是一个更好的例子,说明原始 try/catch 确实 捕获拒绝(不仅仅是同步错误):
猜你喜欢
  • 2018-10-12
  • 1970-01-01
  • 1970-01-01
  • 2017-10-18
  • 2020-02-07
  • 1970-01-01
  • 2018-03-31
  • 2016-12-09
相关资源
最近更新 更多