Vue 子组件不能直接修改 prop 怎么办?

2,598 阅读2分钟

「这是我参与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>

执行流程

  1. 点击open按钮,触发click事件,调用open方法,父组件visible=true
  2. 子组件prop接收,子组件visible=true,子组件显示
  3. 点击close按钮,触发click事件,调用close方法,子组件visible=false,子组件隐藏·

出现问题

乍一看好像没什么问题,然而执行第3步后,控制台出现警告

截屏2021-11-22 21.55.48

并且,再点击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" />

我们的例子中propNameparentData的变量名都是visiblenewProp的值则是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就是这么实现的