Skip to content

组件通信

1. Props / Emits:最基础的父子传值最基础的父子传值

Props:父传子的单向数据流

Details

这是 Vue 的官方推荐通信方式,遵循单向数据流原则,数据只能从上往下流,事件从下往上传。

适用场景:当你需要把配置、用户信息、状态等数据从父组件传递给子组件时。

vue
<!-- 父组件调用 -->
<script>
	// 短评数据
import type { CommentData } from '@/types/comment'

// 准备评论数据 - 现在作为一个对象传递
const commentData = ref<CommentData>({
  list: [
    {
      id: 1,
      avatar: 'https://picsum.photos/id/100/300/240',
      username: '用户名1',
      rating: 4.5,
      date: '2025-12-20',
      location: '山东',
      content: '郑单958可以适应近些年的极端气候,去年种了900亩,今年1200亩!',
      likes: 3051,
    },
    {
      id: 2,
      avatar: 'https://picsum.photos/id/101/300/240',
      username: '用户名2',
      rating: 5,
      date: '2025-12-21',
      location: '河南',
      content: '姜还是老的辣,郑单958可以适应近些年的极端气候,去年种了900亩,今年1200亩!',
      likes: 2100,
    },
    {
      id: 3,
      avatar: 'https://picsum.photos/id/102/300/240',
      username: '用户名3',
      rating: 3.5,
      date: '2025-12-22',
      location: '河北',
      content: '这个品种确实不错,抗病性强,产量稳定',
      likes: 1200,
    },
    {
      id: 4,
      avatar: 'https://picsum.photos/id/103/300/240',
      username: '用户名4',
      rating: 4,
      date: '2025-12-23',
      location: '江苏',
      content: '连续三年种植,效果一直很好',
      likes: 980,
    },
  ],
  total: 102, // 服务器返回的实际总数
})
</script>

<template>
	<DuanPing :comment-data="commentData"></DuanPing>
</template>
vue
<!-- 子组件 -->
<script>
import type { CommentData, CommentItem } from '@/types/comment'
  
// 直接接收-字符串数组形式(最简单)
defineProps(['commentData'])
  
// 带类型验证
defineProps({
  title: String,
  count: {
    type: Number,
    default: 0,
    required: true
  },
  isActive: {
    type: Boolean,
    default: false
  },
  user: {
    type: Object,
    default: () => ({})
  }
})
  
// 官方推荐
// TypeScript 定义 props - 使用明确的接口定义避免类型问题
interface Props {
  commentData?: CommentData
  title?: string
  iconType?: string
  showViewAll?: boolean
  maxDisplay?: number
}

const props = withDefaults(defineProps<Props>(), {
  commentData: undefined, // 不提供默认值,在 computed 中处理
  title: '短评',
  iconType: 'chat',
  showViewAll: true,
  maxDisplay: undefined,
})
</script>
props 专属坑位

上面的类型声明文件中:

ts
// 评论项接口
export interface CommentItem {
  id: number | string
  avatar: string
  username: string
  rating: number
  date: string
  location: string
  content: string
  likes: number
}

// 评论数据接口(包含评论列表和总数)
export interface CommentData {
  list: CommentItem[]
  total: number
}

// 组件 Props 接口
export interface CommentCardProps {
  commentData?: CommentData
  title?: string
  iconType?: string
  showViewAll?: boolean
  maxDisplay?: number
}

其中,绝对不可以有的是:

ts
// 组件 Props 接口
export interface CommentCardProps {
  commentData?: CommentData
  title?: string
  iconType?: string
  showViewAll?: boolean
  maxDisplay?: number
}

因为,interface props 的声明,必须在组件内部,而不可以独立于类型文件中引入使用!否则props为undefined!

为此折腾了俩小时,晕了!

Emits:子传父的事件机制

点击查看详情

适用场景:子组件需要通知父组件有事发生,比如表单提交、按钮点击、输入变化等。

vue
<!-- 子组件 ChildComponent.vue -->
<script>
//定义可触发的事件
interface Emits {
  (e: 'update:modelValue', value: boolean): void
  (e: 'open'): void
  (e: 'close'): void
  (e: 'toggle', value: boolean): void
  (e: 'click-overlay'): void
  (e: 'click-popup'): void
  // === 新增:初始高度归位事件 ===
  (e: 'initial-height-reset'): void
}

const emit = defineEmits<Emits>()

// 切换弹出层状态
const togglePopup = (show?: boolean) => {
  if (isAnimating.value) return

  const targetState = show !== undefined ? show : !isActive.value

  isAnimating.value = true
  isActive.value = targetState

  // === 重置拖拽状态 ===
  if (targetState) {
    resetDragState()
  }

  // === 新增:弹出层切换时重置初始高度 ===
  // 如果弹出层从未激活变为激活(即展开),重置初始高度
  if (targetState && isUsingInitialHeight.value) {
    resetInitialHeight()
  }

  // 传递给父组件
  emit('update:modelValue', targetState)
  emit('toggle', targetState)

  if (targetState) {
    emit('open')
    // 展开后滚动到顶部
    setTimeout(() => {
      if (scrollViewRef.value) {
        scrollViewRef.value.scrollTo({
          top: 0,
          duration: 0,
        })
      }
    }, 50)
  } else {
    emit('close')
  }

  // 动画结束后重置状态
  setTimeout(() => {
    isAnimating.value = false
  }, 300)
}
</script>
vue
<!-- 父组件 Parent.vue -->
<tempalate>
	<!-- 弹出层组件 -->
    <BottomPopup
      ref="popupRef"
      v-model="popupVisible"
      :default-height="80"
      :initial-height="100"
      active-height="85vh"
      @open="handleOpen"  <!-- 这里,使用 @ 符接收子组件的 emit -->
      @close="handleClose"  <!-- 并且指向js逻辑中的具体逻辑 -->
    >
      <!-- 使用插槽传入自定义内容 -->
      <view class="popup-content">
        <view class="section">
          <text class="title">标题一</text>
          <text class="desc">这里是内容描述...</text>
        </view>

        <view class="section">
          <text class="title">标题二</text>
          <text class="desc">更多内容...</text>
        </view>

        <!-- 列表内容 -->
        <view
          v-for="item in list"
          :key="item.id"
          class="list-item"
          @tap="handleItemClick(item)"
        >
          <text>{{ item.name }}</text>
        </view>
      </view>
    </BottomPopup>
</tempalate>

多组件通信(Vue 3.3+ 推荐)

点击查看详情
vue
<template>
  <!-- 使用 v-model 语法糖 -->
  <CustomInput v-model="username" />
  
  <!-- 等价于 -->
  <CustomInput 
    :modelValue="username"
    @update:modelValue="newValue => username = newValue"
  />
</template>

也就是说:

vue
<BottomPopup
  ref="popupRef"
  v-model="popupVisible"
>
  <!-- 插槽内容部分 -->
</BottomPopup>

<!-- 等价于 -->

<BottomPopup
  ref="popupRef"
  :modelValue="popipVisible" 
  @uodate:modelValue="newValue => popuopVisible = newValue"
>
  <!-- 插槽内容部分 -->
</BottomPopup>

2. v-model:双向绑定的语法糖

v-model 在 Vue3 中变得更加强大,支持多个 v-model 绑定

点击查看详情

Vue 2 中的 v-model

vue
<!-- 父组件 -->
<ChildComponent v-model="value" />

<!-- 等价于 -->
<ChildComponent 
  :value="value"
  @input="value = $event"
/>

Vue 3 中的默认 v-model

vue
<!-- 父组件 -->
<ChildComponent v-model="value" />

<!-- 等价于 -->
<ChildComponent 
  :modelValue="value"
  @update:modelValue="newValue => value = newValue"
/>

但 Vue 3 的 v-model 已经超越了"简单语法糖":

特性一:多个 v-model 绑定

vue
<!-- 父组件:可以绑定多个 v-model -->
<UserForm
  v-model:name="userName"
  v-model:email="userEmail"
  v-model:age="userAge"
/>

<!-- 等价于 -->
<UserForm
  :name="userName"
  @update:name="userName = $event"
  :email="userEmail"
  @update:email="userEmail = $event"
  :age="userAge"
  @update:age="userAge = $event"
/>


<!-- 子组件 UserForm.vue -->
<template>
  <input :value="name" @input="updateName($event.target.value)">
  <input :value="email" @input="$emit('update:email', $event.target.value)">
  <input type="number" :value="age" @input="updateAge($event.target.value)">
</template>

<script setup lang="ts">
// 定义 Props 接口
interface Props {
  name: string
  email: string
  age: number
}

// 定义 Emits 类型 -  传统方式(Vue 3.4 之前):需要手动声明 props 和 emits
const emit = defineEmits<{
  (e: 'update:name', value: string): void
  (e: 'update:email', value: string): void
  (e: 'update:age', value: number): void
}>()

// 接收 Props -  传统方式(Vue 3.4 之前): 需要手动声明 props 和 emits
const props = defineProps<Props>()

// 处理方法
const updateName = (value: string) => {
  emit('update:name', value.trim())
}

const updateAge = (value: string) => {
  const age = parseInt(value)
  if (!isNaN(age)) {
    emit('update:age', Math.max(0, age)) // 确保年龄不为负数
  }
}
</script>

v-model的核心优势:

  • 语法简洁,减少样板代码
  • 符合双向绑定的直觉
  • 支持多个v-model绑定
  • 类型安全(配合TypeScript)

适用场景:自定义表单控件(如日期选择器、富文本编辑器)需要双向绑定。

Vue 3.4+:使用 defineModel(不需要手动声明)

Vue 3.4+ 推荐

新的简洁方式(Vue 3.4+ 推荐):

vue
<!-- ChildComponent.vue -->
<script setup>
// 一行代码搞定!不需要 defineProps 和 defineEmits
const modelValue = defineModel()
// 或者带参数的
const title = defineModel('title')
const count = defineModel('count', { type: Number, default: 0 })
</script>

<template>
  <!-- 可以直接用 v-model 绑定 -->
  <input v-model="modelValue">
  <input v-model="title">
  <input v-model="count" type="number">
</template>

这样一来,父组件和子组件,都使用 v-modle 绑定!?!?

需要澄清这个重要概念:

  1. 核心区别:父组件用指令,子组件用变量
vue
<!-- 父组件:使用 v-model 指令 -->
<template>
  <ChildComponent v-model="parentData" />
  <!-- 或者多个 v-model -->
  <ChildComponent 
    v-model:name="userName"
    v-model:age="userAge"
  />
</template>

<!-- 子组件:使用 defineModel() 变量 -->
<script setup>
// 接收父组件的 v-model 绑定
const modelValue = defineModel()           // 对应 v-model
const name = defineModel('name')           // 对应 v-model:name
const age = defineModel('age', { type: Number })
</script>

<template>
  <!-- 在子组件内部使用变量,不是 v-model 指令 -->
  <input :value="name" @input="name = $event.target.value">
  
  <!-- 或者直接用 v-model(这是子组件内部的 v-model) -->
  <input v-model="age">
</template>
  1. 两个层次的 v-model

层次一:父子通信的 v-model(指令)

vue
<!-- 父组件 template 中 -->
<ChildComponent v-model="data" />
<!-- 这是 Vue 指令,建立父子绑定 -->

层次二:子组件内部的 v-model(变量/指令)

vue
<!-- 子组件内部 -->
<script setup>
const data = defineModel()  // 这是变量,接收父组件的绑定
</script>

<template>
  <!-- 这是子组件内部自己用的 v-model 指令 -->
  <input v-model="data">  <!-- 绑定到 defineModel() 返回的变量 -->
</template>
  1. 完整流程示例 - 场景:双向绑定表单
vue
<!-- ParentComponent.vue -->
<template>
  <div>
    <p>父组件数据:{{ formData.name }} - {{ formData.age }}</p>
    
    <!-- 父组件使用 v-model 指令绑定到子组件 -->
    <UserForm 
      v-model:name="formData.name"
      v-model:age="formData.age"
    />
  </div>
</template>

<script setup>
import { reactive } from 'vue'
import UserForm from './UserForm.vue'

// 父组件的数据
const formData = reactive({
  name: '张三',
  age: 25
})
</script>

<!-- UserForm.vue 子组件 -->
<script setup>
// 子组件接收父组件的 v-model 绑定
// 这里 defineModel() 创建的是响应式变量
const name = defineModel('name', { default: '' })
const age = defineModel('age', { type: Number, default: 0 })
</script>

<template>
  <div>
    <!-- 子组件内部可以像普通变量一样使用 -->
    <div>
      <label>姓名:</label>
      <!-- 方式1:直接绑定到变量 -->
      <input :value="name" @input="name = $event.target.value">
    </div>
    
    <div>
      <label>年龄:</label>
      <!-- 方式2:使用子组件内部的 v-model 指令 -->
      <input v-model="age" type="number">
    </div>
    
    <!-- 显示子组件当前的值 -->
    <p>子组件当前值:{{ name }} - {{ age }}</p>
  </div>
</template>
  1. 混淆点:子组件模板中的 v-model
vue
<!-- 子组件中这样写: -->
<input v-model="name">

<!-- 这个 v-model 是:
1. 一个 Vue 指令
2. 但它绑定的是 defineModel() 返回的变量
3. 这个变量已经连接了父组件
-->

实际上相当于:

vue
<!-- 手动实现的等价代码 -->
<script setup>
// defineModel('name') 相当于:
const props = defineProps(['name'])
const emit = defineEmits(['update:name'])

const name = computed({
  get() { return props.name },
  set(value) { emit('update:name', value) }
})
</script>

<template>
  <!-- 这样就能理解为什么可以用 v-model 了 -->
  <input v-model="name">
</template>
  1. 错误理解 vs 正确理解

错误理解:

vue
<!-- 父组件 -->
<ChildComponent v-model="data" />

<!-- 子组件(错误!) -->
<template>
  <!-- 错误:在子组件中对父组件用 v-model -->
  <input v-model="parentData">
</template>

正确理解:

vue
<!-- 父组件:使用指令建立连接 -->
<ChildComponent v-model="parentData" />

<!-- 子组件:使用变量接收连接 -->
<script setup>
const modelValue = defineModel()  // 接收连接
</script>

<template>
  <!-- 内部可以用 v-model 绑定这个变量 -->
  <input v-model="modelValue">
  
  <!-- 或者手动绑定 -->
  <input 
    :value="modelValue"
    @input="modelValue = $event.target.value"
  >
</template>

3. Ref / 模板引用:直接操作组件

适用场景:需要调用子组件方法(如弹窗打开)、聚焦输入框、操作原生元素(如 video 播放)。

点击查看详情
vue
<!-- 父组件 Parent.vue -->
<script setup>
import { ref, onMounted, nextTick } from 'vue'

// 创建 - 弹出层引用
const popupRef = ref<InstanceType<typeof BottomPopup> | null>(null)

// 在页面中直接使用onPageScroll生命周期
onPageScroll(e => {
  const scrollTopRpx = e.scrollTop / 2
  normalizedScrollTop.value = Math.min(100, Math.round(scrollTopRpx))
  if (normalizedScrollTop.value > 30) {
    ifScroll.value = true
  } else {
    ifScroll.value = false
  }
  console.log('🎯 标准化滚动值:', normalizedScrollTop.value)
  console.log('🎯 滚动触发布尔值变化:', ifScroll.value)
  popupRef.value?.resetInitialHeight()
  // popupRef.value?.close()
})
</script>

<tempalate>
	<!-- 弹出层组件 -->
    <BottomPopup
      ref="popupRef" <!-- 这里,使用 ref 进行绑定 -->
      v-model="popupVisible" 
      :default-height="80"
      :initial-height="100"
      active-height="85vh"
      @open="handleOpen"  
      @close="handleClose"  
    >
      <!-- 使用插槽传入自定义内容 -->
    
    </BottomPopup>
</tempalate>
vue
<!-- 子组件 -->
<script>
 // === 新增:重置初始高度 ===
const resetInitialHeight = () => {
  // 如果已经重置过,或者没有在使用初始高度,直接返回
  if (hasInitialHeightReset.value || !isUsingInitialHeight.value) return

  console.log('重置弹出层初始高度')
  hasInitialHeightReset.value = true
  isUsingInitialHeight.value = false

  // 触发归位事件
  emit('initial-height-reset')

  // 注意:这里不需要手动清理事件监听
  // 因为使用的是 { once: true } 选项,事件触发后会自动移除
}
  
// 暴露方法给父组件
defineExpose({
  open: () => togglePopup(true),
  close: () => togglePopup(false),
  toggle: () => togglePopup(),
  isActive,
  scrollToTop: () => {
    if (scrollViewRef.value) {
      scrollViewRef.value.scrollTo({
        top: 0,
        duration: 300,
      })
    }
  },
  // === 新增:手动重置初始高度 ===
  resetInitialHeight: () => {
    resetInitialHeight()
  },
  // === 新增:获取当前是否在使用初始高度 ===
  getIsUsingInitialHeight: () => isUsingInitialHeight.value,
})
</script>

2. Provide / Inject:跨层级数据传递

解决"prop 逐级传递"问题,实现祖先与后代组件的直接通信。

适用场景:当数据需要从顶层组件传递到底层组件,中间隔了好几层(比如主题、用户信息、语言设置)。

点击查看详情
vue
<!-- 根组件 App.vue -->
<template>
  <div id="app">
    <Header />
    <div class="main-content">
      <Sidebar />
      <ContentArea />
    </div>
    <Footer />
  </div>
</template>

<script setup>
import { provide, ref, reactive, computed } from 'vue'

// 提供用户信息
const currentUser = ref({
  id: 1,
  name: '张三',
  role: 'admin',
  permissions: ['read''write''delete']
})

// 提供应用配置
const appConfig = reactive({
  theme: 'dark',
  language: 'zh-CN',
  apiBaseUrl: import.meta.env.VITE_API_URL
})

// 提供方法
const updateUser = (newUserData) => {
  currentUser.value = { ...currentUser.value, ...newUserData }
}

const updateConfig = (key, value) => {
  appConfig[key] = value
}

// 计算属性
const userPermissions = computed(() => currentUser.value.permissions)

// 提供数据和方法
provide('currentUser', currentUser)
provide('appConfig', appConfig)
provide('updateUser', updateUser)
provide('updateConfig', updateConfig)
provide('userPermissions', userPermissions)
</script>
vue
<!-- 深层嵌套的组件 ContentArea.vue -->
<template>
  <div class="content-area">
    <UserProfile />
    <ArticleList />
  </div>
</template>

<script setup>
// 这个组件不需要处理 props,直接渲染子组件
</script>
vue
<!-- 使用注入的组件 UserProfile.vue -->
<template>
  <div class="user-profile">
    <h3>用户信息</h3>
    <div class="profile-card">
      <p>姓名:{{ currentUser.name }}</p>
      <p>角色:{{ currentUser.role }}</p>
      <p>权限:{{ userPermissions.join(', ') }}</p>
      <p>主题:{{ appConfig.theme }}</p>
    </div>
    <button @click="handleUpdateProfile">更新资料</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

// 注入数据和方法
const currentUser = inject('currentUser')
const appConfig = inject('appConfig')
const userPermissions = inject('userPermissions')
const updateUser = inject('updateUser')

const handleUpdateProfile = () => {
  updateUser({
    name: '李四',
    role: 'user'
  })
}
</script>

Provide/Inject的优势

  • 避免Props逐层传递的繁琐
  • 实现跨层级组件通信
  • 提供全局状态和方法的统一管理
  • 提高代码的可维护性

##5. Pinia:现代化状态管理

对于复杂应用,Pinia 提供了更优秀的状态管理方案。

Released under the MIT License.