Skip to content

创建一个可以取消的 promise

1. 方式 1

使用 Promise.withResolvers 来实现。

ts
type Result<T> = {
  readonly data?: T;
  readonly error?: Error;
  readonly cancelled: boolean;
};

type Service<T, K extends any[]> = (...params: K) => Promise<T> & {
  cancel: () => void;
};

const CANCELED_ERROR = Object.freeze(new Error("PROMISE_CANCELLED"));

const buildCancelableTask = <T, K extends any[]>(service: Service<T, K>) => {
  let isCancelled = false;
  let isCompleted = false;
  let servicePromise: ReturnType<Service<T, K>> | null = null;
  const { promise, resolve, reject } = Promise.withResolvers<T>();

  const run = (...params: K): Promise<Result<T>> => {
    if (!isCancelled) {
      servicePromise = service(...params);
      servicePromise.then((data) => resolve(data)).catch((error) => reject(error));
    }

    return promise
      .then((data) => ({
        data,
        cancelled: false
      }))
      .catch((error) => {
        const cancelled = error === CANCELED_ERROR;
        return {
          error: cancelled ? undefined : error,
          cancelled
        };
      })
      .finally(() => {
        isCompleted = true;
      });
  };

  const cancel = () => {
    if (isCompleted) return;
    isCancelled = true;
    reject(CANCELED_ERROR);
    servicePromise?.cancel();
  };

  const isCanceled = () => isCancelled;

  return { run, cancel, isCanceled };
};

export default buildCancelableTask;

Vue3 封装成 useAsyncSequence.ts:

ts
import buildCancelableTask from "../ts/buildCancelableTask";

export default function useAsyncSequence<Args extends unknown[], Data>(
  asyncFn: (...args: Args) => Promise<Data>
) {
  let ret: ReturnType<typeof buildCancelableTask>;
  return () => {
    ret && ret.cancel();
    ret = buildCancelableTask(asyncFn);
    return ret.run();
  };
}

使用

vue
<script setup lang="ts">
import useAsyncSequence from "./hook/useAsyncSequence";

const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));

const run = useAsyncSequence(async () => {
  await sleep(2 * 1000);
  return "Hello";
});

const onClick = async () => {
  const { cancelled, data } = await run();
  console.log("cancelled: ", cancelled);
  console.log("data: ", data);
};
</script>

<template>
  <div @click="onClick"></div>
</template>

另外 promise-withresolvers.ts 可以封装成一个 polyfills:

ts
interface PromiseWithResolvers<T> {
  promise: Promise<T>;
  resolve: (value: T | PromiseLike<T>) => void;
  reject: (reason?: any) => void;
}

if (!Promise.withResolvers) {
  Promise.withResolvers = function <T>(): PromiseWithResolvers<T> {
    let resolve!: (value: T | PromiseLike<T>) => void;
    let reject!: (reason?: any) => void;
    const promise = new Promise<T>((res, rej) => {
      resolve = res;
      reject = rej;
    });
    return { promise, resolve, reject };
  };
}

// 为了让 TypeScript 认识这个新方法,我们需要扩展 Promise 的类型定义
declare global {
  interface PromiseConstructor {
    withResolvers<T>(): PromiseWithResolvers<T>;
  }
}

export {}; // 使这个文件成为一个模块

2. 方式 2

使用 Promise.race 来实现。

ts
type PromiseWithCancel<TData> = Promise<{
  data?: TData;
  error?: Error;
  cancelled: boolean;
}> & {
  cancel: () => void;
};

export function getPromiseWithCancel<TData>(
  promiseFn: () => Promise<TData>
): PromiseWithCancel<TData> {
  let isCancelled = false;
  let cancel = () => {};

  const cancelPromise = new Promise<never>((_, reject) => {
    cancel = () => {
      isCancelled = true;
      reject(new Error("Promise cancelled"));
    };
  });

  const mainPromise = promiseFn().then((data) => {
    if (isCancelled) {
      throw new Error("Promise cancelled");
    }
    return { data, cancelled: false };
  });

  const resultPromise = Promise.race([mainPromise, cancelPromise]).catch((error) => ({
    error: isCancelled ? undefined : error,
    cancelled: isCancelled
  }));

  return Object.assign(resultPromise, { cancel });
}

调用例子:

vue
<script setup lang="ts">
import { getPromiseWithCancel } from "./ts/getPromiseWithCancel";

const fn = async () => {
  const promise = getPromiseWithCancel(async () => {
    await new Promise((r) => setTimeout(r, 1000));
    return "data";
  });

  setTimeout(() => {
    promise.cancel();
  }, 100);

  // 或者等待结果
  const res = await promise;
  console.log(res.data, res.cancelled);
};
fn();
</script>