需求
- 点击一个按钮之后,弹出对话框,显示同步代码的执行进度。
- 当执行失败的时,在这个弹框下方显示一个重试的按钮,点击重新执行。
- 当执行成功后,自动关闭弹框。
由于对话框的打开是在父组件通过$refs调用子组件的main方法实现的,如果重试按钮的click事件直接使用子组件的main方法,则会造成父组件的业务没有执行完。
<!-- 父组件 -->
<template>
<div>
<button @click="openDialog">打开弹框</button>
<my-dialog ref="myDialog" />
</div>
</template>
<script>
import MyDialog from './MyDialog';
export default {
components: { MyDialog },
methods: {
async openDialog(){
let param = { key: 'key', val: 'val'}
let res = await this.$refs.MyDialog.main(param); // 同步业务
await someAsyncFunc(res); //... 拿到同步业务的结果后,执行一些其他业务代码
}
}
}
</script>
在上面父组件调用this.$refs.myComponent.main() 方法后,子组件打开弹窗并执行同步业务,若子组件在执行同步业务的时候发生错误,则抛出error。
这样父组件this.$refs.myComponent.main()这行下面的someAsyncFunc()业务代码便无法执行。用try-catch包裹也不行,因为报错了让下面的someAsyncFunc()继续执行更错了。
如何实现重试业务
1.马上想到子组件保存main()传递的参数,用重试按钮的click事件再次调用main方法,如下
<template>
<div>
<button @click="() => main(tmpParam)">重试</button> <!-- 点击重试后再次调用main方法 -->
</div>
</template>
<script>
export default {
data(){
return {
tmpParam: null
}
},
methods: {
async main(param){
this.tmpParam = param; // 保存传入的参数用于重试按钮执行
try{
await someAsyncFunc(); // 一些同步业务
}catch(err){
console.error(err);
throw err; // 向上抛出异常,阻止父组件进入then
}
}
}
}
</script>
这样将重试的业务逻辑在组件内实现,虽然能成功,但是依旧造成了父组件main()方法后的someAsyncFunc()无法执行,因为父组件调用的main函数及之后的业务流程已经结束了。
2.将重试按钮的点击通过@emit抛给父组件,
父组件接收到后,再次调用整套业务逻辑,相当于关闭了弹框然后再打开。这个方式可能要将父组件的一些业务逻辑分离
<!-- 父组件 -->
<template>
<div>
<button @click="openDialog">打开弹框</button>
<my-dialog ref="myDialog" @retry="handleRetry"/> <!--子组件抛出重试按钮的点击事件-->
</div>
</template>
<script>
import MyDialog from './MyDialog';
export default {
components: { MyDialog },
data(){
return {
tmpParam: {} // 保存用于重试的参数
}
},
methods: {
async openDialog(){
let res = await someAsyncFunc(); // 一些同步方法获取参数
this.tmpParam = { key: res.key, val: res.val} // 保存param用于重试
await restFunc(); // 这里将原来的部分业务代码单独抽出
},
// 重试按钮
async handleRetry(){
await restFunc();
},
// 之后的代码
async restFunc(){
let res = await this.$refs.MyDialog.main(this.tmpParam); // 同步业务
await someAsyncFunc(res); //... 拿到同步业务的结果后,执行一些其他业务代码
}
}
}
</script>
3.我选择在子组件内实现重试逻辑
在父组件执行main()方法,子组件处理报错后使方法执行状态“停止”,在用户点击“重试”后使方法继续执行。
这样父组件相当于执行await this.$refs.MyDialog.main()函数,若报错后,则一直等待,保证了“重试”后下方someAsyncFunc()能够正常执行。
首先想到了使用递归进行处理:
async main(param){
try{
await someAsyncFunc(); // 一些同步业务
}catch(err){
console.error(err);
// 伪代码:
// 若用户点击了重试按钮
return this.main(param); // 递归调用自身
// 否则
throw err; // 向上抛出异常,阻止父组件进入then
}
}
接下来就是怎么让执行状态停止,并监听用户点击了
// 等待用户点击重试
waitForRetry(){
return new Promise(resolve => {
this.retryInterval = setInterval(() => {
if(this.retryClick){
this.retryClick = false;
clearInterval(this.retryInterval);
return resolve(true);
}
}, 500);
);
},
// 点击重试回调
handelRetryClick(){
this.retryClick = true;
},
这里使用了setInterval循环监听标志点击的变量是否变化,变化就调用resolve()使Promise进入下一步。通过这种办法在用户不点击重试的情况下,main函数的代码会一直停留在一行。
子组件的完整代码
<template>
<el-dialog :visible.sync="showDialog" @close="handleClose">
<button v-if="showRetryBtn" @click="retryClick = true"></button>
</el-dialog>
</template>
<script>
export default {
data(){
return {
showDialog: false, // 显示对话框
showRetryBtn: false, // 显示重试按钮
retryClick: false, // 标记重试按钮是否被按下
cancelDialog: false, // 表示是否关闭对话框
retryInterval: null // 监听retryClick是否发生改变的interval
}
},
methods: {
/**
* 主函数
*/
async main(param){
this.showDialog = true; // 展示对话框
try{
await someAsyncFunc();// 一些同步业务
}catch(err){ // 同步业务抛出异常
this.showRetryBtn = true; // 展示重试按钮
if(await waitForRetry()){ // 等待用户点击重试
return this.main(param); // 递归该方法进行返回
}
throw err; // 向上抛出异常,阻止父组件进入then
}
},
// 等待用户点击重试
waitForRetry(){
return new Promise(resolve => {
this.retryInterval = setInterval(() => {
if(this.retryClick){
this.retryClick = false;
clearInterval(this.retryInterval);
return resolve(true);
}
// 也可以再加上取消按钮的监听
if(this.cancelDialog){
this.cancelDialog = false;
clearInterval(this.retryInterval);
return resolve(false);
}
}, 500);
});
},
// 对话框关闭的回调
handleClose(){
this.cancelDialog = true;
clearInterval(this.retryInterval); // 清除interval,promise永不返回
}
}
}
</script>
我担心Promise 不调用resolve/reject后,内存是否会被回收。多次重试后内存是否占用高的问题。
以及el-dialog的@close执行后waitForRetry()是否来得及监听和resolve(false)
总觉得让main函数“暂停”的这个实现有些潦草。希望以后能发现更好的解决办法。