实现 Promise 的封装和多个异步业务的处理

1,341 阅读7分钟

实现 Promise 的封装和多个异步业务的处理

需求:处理业务代码时,会有一些回调函数或者 axios 请求的异步代码,为了代码阅读起来更清晰和更统一,需要正确处理嵌套的回调函数和多个相互依赖的 Promise 任务。

实现思路

  1. 利用 Promise 构造函数封装代码,避免使用回调函数,实现代码高效复用;
  2. 利用 async 函数封装多个异步函数,手动抛出异常情况,实现功能业务函数。

一、Promise 构造函数

Promise 构造器主要用于包装不支持 promise (返回值不是 Promise )的函数。该函数将在构造这个新 Promise 对象过程中,被构造函数执行,而该构造函数executor是一段将输出与 promise 联系起来的自定义代码。

const promise = new Promise(function(resolve, reject) {
  // 一些同步任务...

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise 新建后就会立即执行,但是调用 resolvereject 并不会终结 Promise 的构造函数的执行。这是因为立即 resolvedPromise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。

一般来说,我们调用 resolve 或者 reject 之后就基本完成 Promise 的任务了,不应该再有额外的同步代码,所以建议可以在前面加上 return 确保任务没有意外。

new Promise((resolve, reject) => {
  return resolve(1);
  // 下面不会执行,建议清空
  console.log(2);
});

如果调用 resolve 函数和 reject 函数时带有参数,那么它们的参数会被传递给回调函数。 reject 函数的参数通常是Error对象的实例,表示抛出的错误; resolve 函数的参数一般都是传入正常的值,他们可以在 then 的回调函数中获取到。

promise.then(
  function (value) {
    // 执行promise成功时
  },
  function (error) {
    // 执行promise失败时
  }
);

了解上面的核心概念后,就可以简单封装一个 Promise 任务了。

二、封装确定对话框 Modal 组件

这里参考微信小程序文档的 wx.showModal 模态对话框组件,接受以 Promise 风格进行调用。其中如果用户选择了“确定”按钮,则返回一个对象 { confirm: true, cancel: false } 作为结果;选择了“取消”按钮,则返回对象 { confirm: false, cancel: true } 作为结果。这样在调用时避免传入回调函数的介入,使整个代码更统一和整洁。

src/utils/modal.js

import { Modal } from 'ant-design-vue';
export const MODAL_TITLE = '温馨提示';

/* 封装确定对话框 */
export const useConfirm = ({ title = MODAL_TITLE, content }) => {
  const isConfirm = { confirm: true, cancel: false };
  const isCancel = { confirm: false, cancel: true };
  return new Promise((resolve) => {
    const modal = Modal.confirm({
      title,
      content,
      onOk: () => resolve(isConfirm),
      onCancel: () => resolve(isCancel),
    });
    return modal;
  });
};

下面是封装函数的调用方式,通过 await 返回值确定用户的选择结果,让异步操作更像一个同步操作。

import { useConfirm } from '@utils/modal';

async handleModalClick() {
  this.result = '这里将显示弹窗结果';
  let { confirm } = await useConfirm({ content: '是否发起请求' });
  if (confirm) {
    // 处理确定后的业务...
    this.result = '选择了确定按钮';
  } else {
    // 处理取消后的业务...
    this.result = '选择取消按钮';
  }
},

其实上面的返回值用一个普通的布尔值表示是否选择“确认”就行了,之所以用一个对象主要是希望之后有更多的扩展空间,例如如果获取返回值中有 content 属性,则代表在弹窗中的输入框中用户输入的内容。因为有对象的解构赋值,所以也不会增加代码量。

三、async 函数和 await 命令

async 是 Generator 函数的语法糖,是异步任务的最终方案。 async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

async function timeout(ms) {
  await new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 50);

上面介绍了 Promise 构造函数的 return 返回值,下面分别对 async 和 await 返回值和错误处理进行说明,方便理解之后的业务封装。

async 函数的返回值和错误处理

  1. async 函数内部 return 语句返回的值,会成为then方法回调函数的参数。
  2. async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态。抛出的错误对象会被 catch 方法回调函数接收到。

await 命令后面的内容和错误处理

  1. await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
  2. await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到,此时整个 async 函数会中断执行。
  3. 为了不中断后面的异步操作,可以将 await 放在try...catch结构里面,或者在 await 后面的 Promise 对象再跟一个 catch 方法,处理前面可能出现的已知错误。

四、处理多个异步操作

如果一个业务操作包含多个异步任务,那么我们也可以封装起来,每个异步任务之间独立,借助函数的传参和返回值实现操作的关联。

因为 Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止,所以我们只需要在特定异步任务中抛出错误,在最外层添加一个处理错误机制即可。

import { LoginError, LimitUserError } from '@/utils/userInfo';
/* 处理获取用户登录业务 */
export const handleUserId = async (appId) => {
  const ERROR_USER_ID = -999;
  // 创建异步队列
  const taskQueue = async () => {
    // 获取用户token信息
    let token = await getUserToken({ appId });
    // 根据token获取用户信息
    return await getUserInfo(token);
  };
  // 获取用户userId
  let userId = await taskQueue().catch((err) => {
    // 处理所有错误信息
    if (err instanceof LimitUserError) {
      // 角色无权限
    } else if (err instanceof LoginError) {
      // 登录失败
    } else {
      // 未知错误
    }
    return ERROR_USER_ID;
  });
  // 如果代码出错则退出
  if (userId === ERROR_USER_ID) return;
  // 如果流程顺利则进行...
  this.$router.push({ name: 'home' });
};

每个异步任务都有一些常见的错误,有一些错误是属于函数本身应该解决的,就不应该抛出去,自行解决即可。但是更多的异步任务是应该向上传递错误的,因此我们主动抛出函数错误。

src/store/modules/user.js


import { LoginError, LimitUserError } from '@/utils/userInfo';
const actions = {
  // 通过请求获取token
  async getUserToken({ commit }, params) {
    const res = await apiActions.getTokenApi(params);
    return await new Promise((resolve, reject) => {
      if (/* 异步操作成功 */) {
        return resolve(res);
      }
      /* 异步操作失败 */
      return reject(new LoginError());
    });
  },
  // 通过请求获取用户信息
  async getUserInfo({ commit }) {
    const res = await apiActions.getUserApi();
    return await new Promise((resolve, reject) => {
      if (/* 异步操作成功 */) {
        return resolve(res);
      }
      /* 异步操作成功 */
      return reject(new LimitUserError());
    });
  }
};

上面抛出了两个异步任务的错误,这两个错误可以利用 Error 构造器来自定义异常类型,并使用在业务中使用 instanceof 来检查异常的类型。

src/utils/userInfo.js

/* 用户登录失败错误 */
export const LoginError = function () {
  this.name = 'LoginError';
  this.message = '登录失败,请稍后重试!';
  this.stack = new Error().stack;
};
new LoginError() instanceof LoginError; // true

/* 用户无权限错误 */
export const LimitUserError = function () {
  this.name = 'LimitUserError';
  this.message = '用户无权限,请联系管理员!';
  this.stack = new Error().stack;
};
new LimitUserError() instanceof LimitUserError; // true

五、手动抛出 async 函数的报错

上面我们通过新建一个 Promise 构造函数来决定 async 函数的成功和失败,那么还有没有更简单的方法呢,答案是有的。

  1. 通过 throw new Error() 抛出错误;
  2. 通过 Promise.reject() 抛出错误。
async getUserToken({ commit }, params) {
  const res = await apiActions.getTokenApi(params);
  if (/* 异步操作成功 */) {
    return res;
  }
  // 写法一
  // throw(new LoginError());
  // 写法二
  return Promise.reject(new LoginError());
},

六、处理不存在继发的异步操作

平时在开发过程中,如果过分依赖 async 函数,可以会写出一些耗时的无用代码。

let foo = await getFoo();
let bar = await getBar();

上面代码中,getFoogetBar是两个独立的异步操作(即互不依赖),被写成继发关系。因为只有getFoo完成以后,才会执行getBar,这样就比较耽误时间。

因此,多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。这也是多个异步业务封装的优化内容。

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;