一、问题背景

首先用一个简单的例子来铺垫下背景:

有一个组件Parent ,它有个子组件叫RevertButton ,子组件支持传入isDeleted 属性来控制子组件是展示正常态还是被删除态。伪代码如下所示:

// Parent.vue
<template>
    <RevertButton
        v-model:isDel="isDeleted"
        @deleted="handleButtonDeleted"
        @revert="handleButtonRevert"
    >这是个可删除按钮</RevertButton>
</template>
<script lang="ts" setup>
const isDeleted = ref(false)
const handleButtonDeleted = () => {
    // 在按钮被删除时,执行一些操作
    console.log("RevertButton被删除了")
}
const handleButtonRevert = () => {
    // 在按钮从被删除态恢复时触发
    console.log("RevertButton被恢复了")
}
</script>

在子组件中,会包含一个删除按钮,用户点击删除按钮后按钮展示被删除态,在此状态可以点击恢复按钮再次变为正常态

  • 正常态

Vue3开发模式探索:组件状态变更后emit时机的正确用法

  • 被删除态

Vue3开发模式探索:组件状态变更后emit时机的正确用法

二、错误的子组件状态变更emit写法

关于子组件内部的实现逻辑,其实非常简单,就是在删除按钮的点击事件时将组件状态修改被删除,然后emits("deleted") ,在按钮从被删除态恢复时,修改组件状态,然后emits("revert") 。依照这个思路,很容易写出如下代码:

<template>
    ...
    <button @click="handleDelete">删除</button>
    <button @click="handleRevert">恢复</button>
</template>
<script lang="ts" setup>
// useVModel是vueuse提供的快速实现双向绑定的工具hook
const isDelModel = useVModel("props", "isDel")
const handleDelete = () => {
    isDelModel.value = true
    emits("deleted")                                    
}
const handleRevert = () => {
    isDelModel.value = false
    emits("revert")                                    
}
</script>

仅限于我们的功能而言,似乎已经可以了,但是其实是有问题的,大家可以先思考下问题在哪里?

三、emits依赖事件还是依赖状态?

结合标题,在结合第二部分中的例子,很明显,在前面的例子中,emits依赖事件的。

什么是依赖事件呢?就是emits的执行时机是在事件中。也就是第二部分例子中的handleDeletehandleRevert

那么在本文的例子中,依赖事件会有什么问题呢?

简单的说,就是父组件在监听子组件状态变化时,只能监听到事件引起的状态变化,而监听不到不依赖事件的状态变化

  • 事件引起的状态变化

即在handleDeletehandleRevert 事件中修改状态,同时触发emits 。这时候父组件是可以监听到的。

  • 不依赖事件的状态变化

试想,如果我们在子组件中直接修改isDelModel.value 的值,会发生什么?没错,是无法触发父组件的监听事件的。有的同学可能会说,我可以修改后手动调用emits ,当然可以,但是这样会导致写很多冗余代码,在每一次修改isDelModel.value 后,都手动调用emits

四、组件状态的emits应该和状态绑定

所以,到这里,我们可以总结一个小经验:就是组件状态变化相关的emits,应该和组件状态唯一绑定,不应该依赖任何修改状态的事件

还是拿第二部分的代码为例,我们只需要如下修改即可实现emits和状态绑定:

const handleDelete = () => {
    isDelModel.value = true                                  
}
const handleRevert = () => {
    isDelModel.value = false                             
}
watch(isDelModel, (val, prev) => {
    if (val && !prev) {
         emits("deleted")
         return                 
    }
    if (!val && prev) {
         emits("revert")                
    }
})

这样,不管我们是在子组件内部修改isDelModel.value,还是在父组件修改了isDeleted.value ,都可以正确触发父组件对状态的监听。

五、总结

这里我把状态变化归为两类:

  • 依赖事件的状态变化
  • 状态值被修改引起的变化

同时,emits的执行时机也可以分为两类:

  • 事件引起的emits
  • 状态变更引起的emits,这种情况包括事件

在实际开发中,分清楚是哪一种情况可以帮助我们确定应该在哪里执行emits

本文内容皆来自于日常开发的简单思考,肯定有不完善或是不合理的地方,如有遗漏或者谬误,欢迎指正~