实现 Promise 的封装和多个异步业务的处理
需求:处理业务代码时,会有一些回调函数或者 axios 请求的异步代码,为了代码阅读起来更清晰和更统一,需要正确处理嵌套的回调函数和多个相互依赖的 Promise 任务。
实现思路
- 利用
Promise
构造函数封装代码,避免使用回调函数,实现代码高效复用; - 利用
async
函数封装多个异步函数,手动抛出异常情况,实现功能业务函数。
一、Promise 构造函数
Promise
构造器主要用于包装不支持 promise
(返回值不是 Promise
)的函数。该函数将在构造这个新 Promise
对象过程中,被构造函数执行,而该构造函数executor
是一段将输出与 promise
联系起来的自定义代码。
const promise = new Promise(function(resolve, reject) {
// 一些同步任务...
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise 新建后就会立即执行,但是调用 resolve
或 reject
并不会终结 Promise 的构造函数的执行。这是因为立即 resolved
的 Promise
是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
一般来说,我们调用 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 函数的返回值和错误处理
async
函数内部return
语句返回的值,会成为then
方法回调函数的参数。async
函数内部抛出错误,会导致返回的Promise
对象变为reject
状态。抛出的错误对象会被catch
方法回调函数接收到。
await 命令后面的内容和错误处理
await
命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。await
命令后面的 Promise 对象如果变为reject
状态,则reject
的参数会被catch
方法的回调函数接收到,此时整个async
函数会中断执行。- 为了不中断后面的异步操作,可以将
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
函数的成功和失败,那么还有没有更简单的方法呢,答案是有的。
- 通过
throw new Error()
抛出错误; - 通过
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();
上面代码中,getFoo
和getBar
是两个独立的异步操作(即互不依赖),被写成继发关系。因为只有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;