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

相关推荐
ejinxian34 分钟前
Rust GUI框架Azul与Electron、WebView2
前端·javascript·electron
IT_陈寒1 小时前
Vue的v-for里用index当key,我被自己坑惨了
前端·人工智能·后端
代码不加糖2 小时前
0基础搭建前后端分离项目:实现菜单与界面左右布局
java·前端·javascript·mysql·elementui·mybatis
zhensherlock2 小时前
Protocol Launcher 系列:Tally 快速计数器的深度集成
前端·javascript·typescript·node.js·自动化·github·js
AC赳赳老秦2 小时前
OpenClaw权限管理实操:团队共享Agent,设置操作权限,保障数据安全
服务器·开发语言·前端·javascript·excel·deepseek·openclaw
光影少年3 小时前
Polyline 组件如何绘制渐变区域?
前端·javascript·掘金·金石计划
Pkmer3 小时前
古法编程: React思维模型快速建立
前端·react.js
普通网友3 小时前
JavaScript:ESLint+Prettier 规范代码格式
开发语言·javascript·ecmascript
jiayong233 小时前
第 38 课:任务列表里高亮当前正在查看详情的任务
开发语言·前端·javascript·vue.js·学习
anOnion3 小时前
构建无障碍组件之Spinbutton Pattern
前端·html·交互设计