Vue 自定义组件开发心得-对话框(重试业务递归实现)

本文探讨如何在父组件中实现点击重试功能,避免子组件执行失败导致的业务流程中断。作者介绍了三种方法,包括在子组件内保存参数、通过事件回传给父组件以及使用递归来控制执行状态,最终选择了一种确保重试不影响后续代码执行的方式。
摘要由CSDN通过智能技术生成

需求

  1. 点击一个按钮之后,弹出对话框,显示同步代码的执行进度。
  2. 当执行失败的时,在这个弹框下方显示一个重试的按钮,点击重新执行。
  3. 当执行成功后,自动关闭弹框。

由于对话框的打开是在父组件通过$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函数“暂停”的这个实现有些潦草。希望以后能发现更好的解决办法。

参考资料

  1. 一直没有resolve也没有reject的Promise会造成内存泄露吗?
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在Vue实现自定义组件的双向绑定(v-model),你可以使用`model`选项。下面是一个简单的例子,演示如何创建一个自定义组件并将其包装在v-model中使用: ```html <!-- 父组件 --> <template> <div> <custom-input v-model="message"></custom-input> <p>Message: {{ message }}</p> </div> </template> <script> import CustomInput from './CustomInput.vue'; export default { components: { CustomInput }, data() { return { message: '' } } } </script> ``` ```html <!-- CustomInput.vue --> <template> <input :value="value" @input="$emit('input', $event.target.value)"> </template> <script> export default { props: ['value'] } </script> ``` 在上面的例子中,父组件通过`v-model="message"`将`message`作为属性传递给子组件`CustomInput`。子组件接收该属性作为`value`,并在输入事件触发时通过`$emit('input', $event.target.value)`将新的值传递回父组件。 这样,当用户在子组件中输入文本时,父组件的`message`属性会自动更新,实现了双向绑定。 请注意,使用`v-model`时,子组件内部的属性名必须为`value`,而不是其他名字。这是因为`v-model`默认会使用`value`作为属性名来传递值,并使用`input`事件来更新值。如果你想要使用不同的属性名,可以在子组件中使用`model`选项来自定义。 ```html <!-- CustomInput.vue --> <template> <input :value="internalValue" @input="updateValue($event.target.value)"> </template> <script> export default { model: { prop: 'customValue', event: 'customInput' }, props: { customValue: String }, computed: { internalValue: { get() { return this.customValue; }, set(newValue) { this.$emit('customInput', newValue); } } }, methods: { updateValue(value) { this.internalValue = value; } } } </script> ``` 通过在子组件中使用`model`选项,你可以自定义属性名和事件名,以实现更灵活的双向绑定。在上面的例子中,父组件可以这样使用: ```html <custom-input v-model="message"></custom-input> ``` 这会将父组件的`message`属性传递给子组件的`customValue`属性,并使用`customInput`事件来更新值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值