实现一个可取消的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

相关推荐
叫我一声阿雷吧8 分钟前
JS 入门通关手册(45):浏览器渲染原理与重绘重排(性能优化核心,面试必考
javascript·前端面试·前端性能优化·浏览器渲染·浏览器渲染原理,重排重绘·reflow·repaint
大家的林语冰18 分钟前
《前端周刊》尤大开源 Vite+ 全家桶,前端工业革命启动;尤大爆料 Void 云服务新产品,Vite 进军全栈开发;ECMA 源码映射规范......
前端·javascript·vue.js
jiayong2335 分钟前
第 8 课:开始引入组合式函数
前端·javascript·学习
田八38 分钟前
聊聊AI的发展史,AI的爆发并不是偶然
前端·人工智能·程序员
zhanghongbin011 小时前
AI 采集器:Claude Code、OpenAI、LiteLLM 监控
java·前端·人工智能
IT_陈寒1 小时前
Python的列表推导式里藏了个坑,差点让我加班到凌晨
前端·人工智能·后端
吴声子夜歌1 小时前
ES6——正则的扩展详解
前端·mysql·es6
天若有情6731 小时前
【C++原创开源】formort.h:一行头文件,实现比JS模板字符串更爽的链式拼接+响应式变量
开发语言·javascript·c++·git·github·开源项目·模版字符串
天***88521 小时前
Edge 浏览器离线绿色增强版+官方安装包,支持win7等系统
前端·edge
漫游的渔夫2 小时前
别再直接 `json.loads` 了!AI 返回的 JSON 坑位指南
前端·人工智能