Vue 组件通信
Vue 组件通信
在 Vue 项目开发中,理解组件间的数据传递是构建可维护应用的基础。这个博客从项目结构出发,整理父子组件通信的原理、单向数据流的限制,以及如何使用 v-model 实现双向绑定。
项目结构
在标准的 Vue 项目(如基于 Vite 或 Webpack 构建)中,src 目录下的结构通常按照下面的方式组织:
src/views(或pages):这里的每一个.vue文件通常对应路由系统中的一个页面。src/components:这里存放的是通用组件。
类似于在 WPF 开发中,有 Page(页面),也有 UserControl(用户控件)。当需要一个通用的表单结构、一个数据展示卡片,或者一个自定义的按钮时,我们不应该把代码写死在页面里,而是将其封装在 src/components 中。核心的想法就是,组件不是页面,而是页面中的积木。我们希望组件能不知情地被放置在任何页面中复用。
数据驱动的组件控制
Vue 是基于 MVVM(Model-View-ViewModel)架构的,核心理念是数据驱动视图。在传统 jQuery 时代,要显示一个弹窗,可能会直接操作 DOM(如 $('#dialog').show())。但在数据驱动的架构中,我们要摒弃这种思维。我们控制的是变量值。比如我们要控制页面中某个组件的显示(建立)或者消失(销毁),比如当点击某个”添加“按钮的时候,弹出一个表单组件,那么就可以使用通过该百年 v-if 或者 v-show 的值来控制:
v-if:当变量为true时,组件被创建(DOM 插入);变量为false时,组件被销毁(DOM 移除)。v-show:仅切换 CSS 的display属性,组件始终存在于 DOM 中。
1 | <script setup> |
这里引出了一个问题:父组件持有 drawerOpen 变量,但如果我想在子组件(抽屉内部)点击“关闭”按钮来改变这个变量,该怎么办? 这就涉及到了组件通信。
父传子:Props(属性传递)
为什么不能直接访问?
Vue 的组件设计遵循组件隔离原则。虽然 JavaScript 中有闭包的概念,但在 Vue 中,import 引入的子组件仅仅是一个组件的定义(对象或类)。在父组件模板中使用 <Child /> 时,Vue 会在底层实例化这个组件。父组件和子组件的作用域是完全独立的。子组件无法直接读取父组件的变量,除非父组件显式地“递”过去。
这里涉及到了词法作用域和动态作用域的区别,可以参考前一篇博客。
使用 defineProps 接收数据
在子组件中,需要使用 defineProps 宏来声明“我愿意接收哪些数据”。
父组件:
1 | <!-- 使用 :title (即 v-bind:title) 传递变量,使用 title="..." 传递纯字符串 --> |
子组件 (Child.vue):
1 | <script setup lang="ts"> |
Attribute 透传 (Fallthrough Attributes)
如果在父组件传递了某个属性(例如 class="active" 或 id="card-1"),但子组件没有通过 defineProps 声明它,Vue 会自动把这些属性“透传”并挂载到子组件的根元素上。
但这种透传仅限于 HTML 属性,无法在 <script setup> 逻辑中作为数据使用。
子传父:Emits(事件通知)
Vue 严格遵循单向数据流原则:数据应该从父组件流向子组件,子组件不应直接修改父组件的数据。这是因为如果在子组件内部执行 props.title = '新标题',由于对象引用的关系,父组件的数据可能也会变。但父组件对此毫不知情。如果父组件把这个数据同时传给了 5 个子组件,一旦出现数据异常,就很难追踪是哪个子组件“偷偷”修改了数据。
因此,在开发环境下,如果我们尝试修改 props,Vue 会在控制台抛出警告:
[Vue warn]: Set operation on key "title" failed: target is readonly.
既然不能直接改,子组件如果想改变数据,必须通知父组件,让父组件自己去改。这就是 Emit(发射事件) 机制。
基础写法:Prop + Emit
这是一个标准的“请求-响应”模式。
子组件 (MyChild.vue): 1
2
3
4
5
6
7
8
9
10
11
12
13<script setup lang="ts">
// 1. 声明接收的数据
defineProps<{ title: string }>()
// 2. 声明即将会触发的事件 'update:title'
// 这里的 update:title 只是一个事件名,你可以叫它 'change-title' 或任何名字
const emit = defineEmits<{'update:title': [value: string]}>()
const changeTitle = () => {
// 向外喊话:我要改 title,新值是 'New Title'
emit('update:title', 'New Title')
}
</script>
父组件: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<script setup lang="ts">
import { ref } from 'vue'
import MyChild from './MyChild.vue'
const pageTitle = ref('初始值')
// 处理函数:接收子组件传来的 newValue
const handleUpdate = (newValue: string) => {
pageTitle.value = newValue
}
</script>
<template>
<MyChild
:title="pageTitle"
@update:title="handleUpdate"
/>
</template>
v-model 实现双向绑定
上面的写法虽然标准,但非常啰嗦:我们要写一个 prop,还要写一个事件监听,还要写一个处理函数。Vue 提供了 v-model 指令作为语法糖,完美解决了这个问题。
当我们写 <MyChild v-model:title="pageTitle" /> 时,Vue 编译器会自动帮我们展开成以下代码:
1 | <MyChild |
它自动完成了两件事: 1. 传递名为 title 的 prop。 2. 监听名为 update:title 的事件,并自动将事件参数赋值给 pageTitle。
父组件: 1
2
3
4
5
6
7
8
9
10
11
12
13<script setup lang="ts">
import { ref } from 'vue'
import MyChild from './MyChild.vue'
const pageTitle = ref('初始标题')
</script>
<template>
<!-- 简洁的双向绑定 -->
<MyChild v-model:title="pageTitle" />
<p>父组件中的值:{{ pageTitle }}</p>
</template>
子组件 (MyChild.vue) - 保持不变:
只要子组件遵循 Prop 为 xxx 且 Emit 事件为 update:xxx 的命名规范,v-model 就能自动生效。
如果我们省略参数,直接写 <MyChild v-model="pageTitle" />,Vue 会默认使用以下名称: * Prop 名称:modelValue * 事件名称:update:modelValue
子组件写法调整: 1
2
3
4
5
6
7
8
9<script setup lang="ts">
// 接收 modelValue
defineProps<{ modelValue: string }>()
const emit = defineEmits<{'update:modelValue': [val: string]}>()
function update() {
emit('update:modelValue', '新值')
}
</script>