「这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战」
Vue 子组件不能直接修改 prop 怎么办?
场景描述
业务场景抽象如下:
点击父组件中的按钮open,子组件v-show=true
点击子组件中的按钮close,子组件v-show=false
代码复现
简易代码如下:
父组件
<template>
<div id="app">
<button @click="open">open</button>
<HelloWorld :visible="visible" />
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
data() {
return {
visible: false
}
},
components: {
HelloWorld
},
methods: {
open() {
this.visible = true
}
}
}
</script>
子组件
<template>
<div>
<div v-show="visible" style="height: 200px; width: 200px; background: pink">
<button @click="close">close</button>
</div>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
visible: Boolean,
},
methods: {
close() {
this.visible = false
}
}
};
</script>
执行流程
- 点击
open按钮,触发click事件,调用open方法,父组件visible=true - 子组件
prop接收,子组件visible=true,子组件显示 - 点击
close按钮,触发click事件,调用close方法,子组件visible=false,子组件隐藏·
出现问题
乍一看好像没什么问题,然而执行第3步后,控制台出现警告:
并且,再点击open按钮,子组件不再出现
通过控制台警告信息可知,Vue 不允许直接修改 prop
官方文档
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
结论:父子 prop 之间形成单向数据流,只能通过改变父级组件改变子组件的 prop
解决办法
既然无法通过直接修改子组件到visible来达到目的,那么我们转换一下思路,有没有什么办法能先改变父组件的visible,从而通过单向数据流间接改变子组件的visible?
我想你应该想到了:$emit
我们可以使用$emit方法触发父组件的方法来改变visible的值
// 子组件 close 方法
close() {
this.$emit("close")
}
<!-- 父组件修改 -->
<HelloWorld :visible="visible" @close="visible = false" />
更进一步
重新理解上面的需求,其实我们想要的是对一个 prop 进行“双向绑定”
对于这种“双向绑定”一个 prop 的需求,Vue 推荐我们这样使用$emit
// 子组件
this.$emit("update:propName", newProp)
<!-- 父组件 -->
<Component v-bind:propName="parentData" v-on:update:propName="parentData = $event" />
我们的例子中propName和parentData的变量名都是visible,newProp的值则是false,所以这么改:
// 子组件 close 方法
close() {
this.$emit("update:visible", false)
}
<!-- 父组件修改 -->
<HelloWorld :visible="visible" @update:visible="visible = $event" />
sync 修饰符
为了方便起见,Vue 为上面update:propName这种模式提供了一个缩写:.sync修饰符
// 子组件 close 方法
close() {
this.$emit("update:visible", false)
}
<!-- 父组件修改 -->
<HelloWorld :visible.sync="visible" />
补充
我们描述的这种场景,其实跟常用的dialog组件一样,可以阅读一下element-ui的源码,他的dialog就是这么实现的