实现一个可取消的promise

背景

实现一个函数 接受一个promise 将一个promise转换为一个可取消的promise。

分析

首先我们返回的必然是一个符合 promise A+ 规范的promise对象,需要支持所有的promise api和调用方式。

cancel

这个promise需要可以取消 我们可以在这个返回的实例上添加一个cancelapi, 当用户调用这个api表明要取消当前的promise.

因为promise的执行特性 本质上一个存储状态和结果以及回调函数的容器

具体参考这里js三座大山之异步三promise本质

而状态是可以同步改变的也可以异步改变 但是读取结果一定是异步的。

例如

javascript 复制代码
const p3 = () =>
  new Promise((resolve) => {
    console.log("Promise内部(同步)");
    resolve(33333);
  });

console.log("开始");
p3().then((res) => {
  console.log("test res", res);  // 异步输出
});
console.log("结束");

输出顺序会是这样的:

bash 复制代码
开始
Promise内部(同步)
结束
test res 33333

这显示了即使Promise的状态改变是同步执行的(即,resolve(33333)时),此时Promise内部容器的状态就已经发生了改变 但是通过.then()注册的回调函数是在当前JavaScript执行上下文之外,也就是异步执行的。这是Promise规范设计的一个重要部分,旨在确保Promise处理总是以预测的方式异步进行,即使Promise立即兑现。

isCancel

所以当调用promise获取结果的时候 可能在没有来得及执行cancel时 promise的状态就已经被改变了。

因此 我们需要一个isCancel的api来判断 当调用了cancel后当前的promise是否真的被取消了。

Promise.race + Symbol

当调用cancel时 promise被取消 实际上我们会通过reject去更改状态 这会导致promise报错抛出异常 为了不影响业务代码 所以我们需要通过catch来捕获 并验证这个异常是用户主动取消引起的还是程序执行引起的。如若果是用户主动取消的 说明是符合我们预期的.

所以我们需要在调用cancel时 通过Symbol作为唯一标记,然后在catch里面比较 判断这个异常是否是我们主动抛出的。

总结下:

返回的promise实例 应该挂载两个api

  1. cancel 用于实现取消的逻辑
  2. isCancel 用于判断是否真的取消了
  3. cancel实现通过Promise.race + Symbol

代码

ini 复制代码
/**
 * 将一个promise转换为一个可取消的promise
 * @param {Promise} task 希望被转换的promise实例
 * @returns {Promise} 返回具有cancel()&isCancel()的promise对象
 */
export const TaskCancelable = (task: Promise<any>) => {
    let _reject: Resolve;
    let isCancel = false;
    const _status = Symbol("cancel");
    const cancelP = new Promise((resolve, reject) => {
      _reject = reject;
    });
    const p = Promise.race([task, cancelP]) as PromiseCancel;
    /***
     * 调用cancel时可能promise状态已经变为成功,
     * 所以不能在cancel里面改变isCancel
     * 只有catch的原因是cancel才代表被取消成功了
     */
    p.catch((reason) => {
     // 注意这里的比较 只有报错的原因是我们的symbol 才是用户手动取消的
      if (reason === _status) { 
        isCancel = true;
      }
    });
  
    p.cancel = () => {
      _reject(_status);
      return p;
    };
    p.isCancel = () => {
      return isCancel;
    };
    return p;
  };

使用

主动取消的情况

javascript 复制代码
const p3 = () =>
  new Promise((resolve) => {
    setTimeout(() => {
      resolve(33333);
    }, 2000);
  });
const cancelP = TaskCancelable(p3());

cancelP
  .cancel()
  .then((res) => {
    console.log("获取结果 res", res);
  })
  .catch((reason) => {
    if (cancelP.isCancel()) {
      // true
      console.log("promise 被用户主动cancel了: ", cancelP.isCancel());
    } else {
      console.log("这里捕获业务异常 reason", reason);
    }
  })
  .finally(() => {
    console.log("finally done"); // true
  });
  
打印结果:
promise 被用户主动cancel了:  true
finally done

不取消的情况

javascript 复制代码
cancelP
  .then((res) => {
    console.log("获取结果 res", res);
  })
  .catch((reason) => {
    if (cancelP.isCancel()) {
      // true
      console.log("promise 被用户主动cancel了: ", cancelP.isCancel());
    } else {
      console.log("这里捕获业务异常 reason", reason);
    }
  })
  .finally(() => {
    console.log("finally done"); // true
  });

打印结果:
获取结果 res 33333
finally done

reject的情况

javascript 复制代码
const p3 = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(33333);
    }, 2000);
  });
const cancelP = TaskCancelable(p3());

cancelP
  .then((res) => {
    console.log("获取结果 res", res);
  })
  .catch((reason) => {
    if (cancelP.isCancel()) {
      // true
      console.log("promise 被用户主动cancel了: ", cancelP.isCancel());
    } else {
      console.log("这里捕获业务异常 reason", reason);
    }
  })
  .finally(() => {
    console.log("finally done"); // true
  });

// 打印结果 
这里捕获业务异常 reason 33333
finally done

接口其他Promise api的情况

javascript 复制代码
const p1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve(1111);
  }, 1000);
});

const p2 = new Promise((resolve) => {
  setTimeout(() => {
    resolve(2222);
  }, 1500);
});

const p3 = () =>
  new Promise((resolve) => {
    setTimeout(() => {
      resolve(33333);
    }, 2000);
  });
const cancelP = TaskCancelable(p3());

Promise.allSettled([cancelP, p1, p2]).then(res => {
  console.log('test res: ', res);
})

打印结果:
test res:  [
  { status: 'fulfilled', value: 33333 },
  { status: 'fulfilled', value: 1111 },
  { status: 'fulfilled', value: 2222 }
]

参考

task-cancelable

相关推荐
陈随易28 分钟前
Vue 编译器换掉 source map,快了三四成
前端·后端·程序员
ZhengEnCi2 小时前
12 种高级 UI 交互效果及对应 AI 提示词:从按压回弹到底部抽屉的物理感教学
前端·css·交互设计
nice_lcj5204 小时前
Java Lambda 表达式入门:怎么写、怎么用、常用方法有哪些
java·开发语言·后端·面试
IT_陈寒4 小时前
Redis大key删除为什么会导致服务卡顿?我的线上事故复盘
前端·人工智能·后端
系统集成-鹰翔4 小时前
ERP图片改名后仍命中识别缓存:缓存键、十分钟到期与调用计数
javascript·经验分享·后端
用户2705077725245 小时前
MCP 最小 Server 实测:3 个 JSON-RPC 报错与修法
前端
用户499335025075 小时前
一人VIP账号多人共用:Web 代理里的会话共享怎么实现
前端
用户老李5 小时前
无线电自学教程 05:第一台收音机——从矿石机到 SDR
前端
IPIPD5 小时前
Node.js请求遇到503怎么重试?先解析Retry-After,再做有上限退避
前端
葡萄城技术团队5 小时前
dsh-spreadjs-editor 的集成链路:把 SpreadJS Designer 嵌进 Agent 工作台
前端