1. Props - 父传子

父组件(Parent.vue)
<template>
  <div>
    <h2>父组件</h2>
    <Child :msg="parentMsg" :count="count" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

// 父组件数据
const parentMsg = ref('我是父组件传递的消息')
const count = ref(100)
</script>
子组件(Child.vue)
<template>
    <p>父组件传递的msg:{{ msg }}</p>  <p>父组件传递的count:{{ count }}</p>
</template>

<script setup>
// const props = defineProps(['msg', 'count'])  // 方式1:简洁声明(仅指定属性名)
const props = defineProps({  // 方式2:带类型校验(推荐)
  msg: {
    type: String,
    required: true, // 是否必传
    default: '默认消息' // 默认值(非必传时生效)
  },
  count: {
    type: Number,
    validator: (value) => { // 自定义校验
      return value >= 0
    }
  }
})
console.log(props.msg, props.count) // 访问props数据(组合式API中props是响应式的)
</script>

注意

  • defineProps<script setup>中无需导入,直接使用。
  • Props 是单向数据流,子组件不能直接修改 props,若需修改,应通知父组件修改(或在子组件内用ref接收 props 并作为本地数据)。

2. 自定义事件 (emit) - 子传父

        子组件通过触发自定义事件,将数据传递给父组件,父组件通过v-on(简写@)监听事件。

子组件(Child.vue)
  • defineEmits:子组件声明事件名
  • emit:子组件的实际触发事件,顺带传递数据(名字随便取,推荐短横线,因为HTML 里不区分大小写)
<template>
  <div>
    <h3>子组件</h3>
    <button @click="sendDataToParent">向父组件传数据</button>
  </div>
</template>

<script setup>
// 方式1:简洁声明(仅指定事件名)
// const emit = defineEmits(['child-click', 'send-data'])

// 方式2:带校验(推荐)
const emit = defineEmits({
  'child-click': null, // 无参数校验
  'send-data': (data) => { // 校验参数
    if (data && data.content) {
      return true
    } else {
      console.error('传递的数据格式错误')
      return false
    }
  }
})

const sendDataToParent = () => {
  // 触发自定义事件,可传递多个参数
  emit('child-click')
  emit('send-data', {
    content: '我是子组件传递的数据',
    num: 666
  })
}
</script>
父组件(Parent.vue)

        父组件的@事件名 = :监听子组件的事件,执行自己的逻辑

<template>
    <p>子组件传递的数据:{{ childData.content }}</p>
    <Child 
      @child-click="handleChildClick" 
      @send-data="handleSendData" 
    />
</template>

<script setup>
    ...
    const childData = ref({})

    const handleChildClick = () => {
      console.log('子组件触发了child-click事件')  // 监听子组件的child-click事件
    }

    const handleSendData = (data) => {
      childData.value = data  // 监听子组件的send-data事件,接收参数
    }
</script>

注意

  • defineEmits<script setup>中无需导入,直接使用。
  • 事件名推荐使用kebab-case(短横线命名),与 HTML 原生事件命名风格保持一致。

3. v-model 双向绑定 - 双向通信

v-model 可以在组件上使用以实现双向绑定。

v-model 可以在组件上使用以实现双向绑定。

从 Vue 3.4 开始,推荐的实现方式是使用 defineModel() 宏:

Child.vue
​
<script setup>
const model = defineModel()

function update() {
  model.value++
}
</script>

<template>
  <div>Parent bound v-model is: {{ model }}</div>
  <button @click="update">Increment</button>
</template>

​
Parent.vue
<Child v-model="countModel" />

   defineModel() 返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,能起到在父组件和当前变量之间的双向绑定的作用。这意味着你也可以用 v-model 把这个 ref 绑定到一个原生 input 元素上,在提供相同的 v-model 用法的同时轻松包装原生 input 元素:

<script setup>
const model = defineModel()
</script>

<template>
  <input v-model="model" />
</template>
多个 v-model 绑定

利用刚才在 v-model 的参数小节中学到的指定参数与事件名的技巧,我们可以在单个组件实例上创建多个 v-model 双向绑定。组件上的每一个 v-model 都会同步不同的 prop,而无需额外的选项:

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

详情请查看vue3官方文档: 组件 v-model | Vue.js

4. Refs 访问 - 父访问子

        父组件通过ref获取子组件实例,从而访问子组件的属性和方法。子组件需通过defineExpose暴露需要被父组件访问的内容(<script setup>中组件内部的属性 / 方法默认是私有的)。

 在这个场景里,ref会用两次(但作用不同):

用法作用
const xxx = ref()定义响应式变量(比如存数字、字符串、对象,值存在.value里)
<Child ref="xxx" />给组件 / 元素 “贴标签”,通过xxx.value能拿到组件实例 / 元素 DOM 对象
子组件(Child.vue)

先写子组件,重点是 “定义变量 / 方法 + 用 defineExpose 暴露”:

<script setup>
    import { ref } from 'vue'
    const childNum = ref(0) // 1. 用ref定义子组件自己的响应式变量(这是ref的第一个用法:定义响应式数据)
    const addNum = () => {  // 2. 定义子组件自己的方法
      childNum.value++ 
    }
    defineExpose({ // 3.用defineExpose暴露想让父组件访问的内容(变量/方法)
      childNum, 
      addNum   
    })
</script>
父组件(Parent.vue)

        父组件的核心是 “给子组件贴 ref 标签 + 通过 ref 拿到子组件实例 + 访问暴露的内容”。ref 变量名必须和模板里的 ref 标签名一致 !

<template>
  <div>
    <!-- 1. 给子组件贴ref标签(这是ref的第二个用法:标记组件)  标签名是childRef,父组件通过这个名字找子组件-->
    <Child ref="childRef" />
    <button @click="accessChild">点击访问子组件的内容</button>
    <p>父组件拿到的子组件数字:{{ childNumFromSon }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'

// 2. 定义ref变量(第一个用法),和子组件的ref标签名一致! 这个变量是用来存子组件实例的,初始值null(还没找到子组件)
const childRef = ref(null)

const childNumFromSon = ref(0)// 父组件自己的变量,用来存从子组件拿到的数字

// 3. 定义方法:访问子组件的内容
const accessChild = () => {
  // 关键:通过childRef.value拿到子组件实例! 只有组件挂载后,childRef.value才不是null!
  const sonInstance = childRef.value

  // 4. 访问子组件暴露的变量(子实例上的 ref, Vue 自动解包)
  childNumFromSon.value = sonInstance.childNum

  // 5. 调用子组件暴露的方法
  sonInstance.addNum() // 调用后子组件的childNum会+1
}

// 【避坑】可选:挂载后验证(组件挂载完成后,childRef才会指向子组件)
onMounted(() => {
  console.log('组件挂载后,子组件实例:', childRef.value) 
  // 打印能看到子组件暴露的childNum和addNum
})
</script>
细节:
1、为什么childRef.value一开始是null

        Vue 组件渲染是 “异步” 的:父组件先创建childRef = ref(null),然后才开始渲染子组件;

        只有等子组件挂载完成(onMounted 钩子触发后),childRef.value才会指向子组件实例;

        正确做法:在onMounted里访问;在按钮点击、定时器等 “异步操作” 里访问(用户点击时组件肯定挂载好了)。

2、子组件为什么要写 defineExpose?

   <script setup>是 Vue3 的 “语法糖”,里面定义的变量 / 方法默认是 “私有” 的(保护组件封装性);

   defineExpose的作用就是 “把私有内容公开给父组件”,想让父组件访问什么,就暴露什么。

核心区别对比表
方式数据流向核心作用组件封装性推荐优先级核心特点
Props(父传子)父 → 子父给子传递初始化 / 配置性数据高(单向)最高子只能读、不能改,要改只能通知父改(符合 Vue 单向数据流)
自定义事件(子传父)子 → 父子给父传递事件 / 状态变更数据子只触发事件、不关心父怎么处理,父只监听事件、不侵入子内部
Ref + defineExpose父 ← 子(主动)主动访问 / 操作子的属性 / 方法低(侵入)父能直接拿到子的实例,读属性、调方法,甚至改子的内部数据(慎用)

5. Provide/Inject - 跨层传递(依赖注入)

   provide/inject是 Vue3 专门解决跨层级组件通信的方案(比如:父 → 子 → 孙,甚至更深),跳过中间组件,避免 “Props 层层透传”(比如爷爷传父、父再传孙)的繁琐。

  • provide(提供):上层组件(比如爷爷 / 父)“发布” 一些数据 / 方法,标记为 “可被后代组件获取”;
  • inject(注入):下层组件(比如孙子 / 子)“订阅” 这些数据 / 方法,不用管中间层级(比如父组件)是否传递,直接拿过来用;
1、上层组件 —— 提供数据(provide)
provide(/* 注入名 */ 'message', /* 值 */ 'hello!') 

   provide() 函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol; 后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。请确保 provide() 是在 setup() 同步调用的

<script setup>
import { provide, ref, reactive } from 'vue' ...
// 1. 提供「非响应式数据」(不推荐,下层拿不到更新)
provide('staticMsg', '我是父组件提供的静态消息')
// 2. 提供「响应式数据」(推荐!下层能实时拿到更新)
const dynamicCount = ref(0) // ref响应式
provide('dynamicCount', dynamicCount)
const userInfo = reactive({ name: '小明', age: 18 }) // reactive响应式
provide('userInfo', userInfo)
// 3. 提供「方法」(让下层组件能修改上层数据)
provide('updateCount', (value) => {
  dynamicCount.value = value // 修改上层的响应式数据
})
</script>
2、下层组件 —— 注入数据(inject)
<script setup>
import { inject } from 'vue'
// 1. 注入数据:第一个参数是provide的key,第二个参数是「默认值」(上层没提供时生效)
const staticMsg = inject('staticMsg', '默认消息')
// 2. 注入响应式数据(拿到的是原始ref/reactive,自动响应更新)
const dynamicCount = inject('dynamicCount', ref(0)) // 给默认值也推荐用ref,保持类型一致
const userInfo = inject('userInfo', reactive({ name: '默认名', age: 0 }))
// 3. 注入上层提供的方法
const updateCount = inject('updateCount')
// 4. 直接修改reactive数据(因为是引用类型,上层也会同步更新)
const addUserAge = () => {
  userInfo.age++
}
</script>
细节

(1)响应式是核心:上层提供非响应式数据,下层拿到后,上层再修改这个值,下层不会更新;上层提供响应式数据,下层拿到的是 “引用”,上层 / 下层修改,双方都会同步更新

        ✅ 推荐做法:所有需要更新的 provide 数据,都用响应式数据:refreactive包裹。

(2)默认值的写法(避免类型不一致)

(3)多人协作时,provide/inject 的 key 推荐抽成常量(避免拼写错误)

// constants/injectKeys.js
export const INJECT_KEYS = {
  STATIC_MSG: 'staticMsg',
  DYNAMIC_COUNT: 'dynamicCount',
  UPDATE_COUNT: 'updateCount'
}
// 父组件
import { INJECT_KEYS } from './constants/injectKeys.js'
provide(INJECT_KEYS.DYNAMIC_COUNT, ref(0))

// 孙组件
import { INJECT_KEYS } from './constants/injectKeys.js'
const dynamicCount = inject(INJECT_KEYS.DYNAMIC_COUNT, ref(0))

(4)下层修改上层数据的两种方式(推荐第二种)

        直接修改reactive数据(简单但不规范);上层提供方法,下层调用(推荐!符合 “单向数据流”)

适用场景(优先用)
  1. 跨多层级通信:比如 “爷爷 → 父 → 子 → 孙”,需要把数据传给孙组件,用 Props 会层层透传(父组件明明用不到,却要帮传),此时用provide/inject最简洁;
  2. 全局级通信(轻量):比如全局的主题配置(亮色 / 暗色)、用户登录状态、权限信息等,可在 App.vue(根组件)provide,所有页面 / 组件都能 inject;

    注:如果是复杂的全局状态(比如购物车、用户信息),推荐用 Pinia/Vuex,而非provide/inject

  3. 组件库开发:比如封装一个表单组件库,表单根组件(Form)provide 配置,表单项组件(FormItem)inject 配置,无需用户手动传 Props。

对比(快速选型)

通信方式数据流向核心本质组件封装性适用层级核心优势核心劣势典型适用场景
Props父 → 子(单向)父给子传递配置 / 初始化数据,子只读不直接改父子(直接)规范、单向数据流、易调试跨层级需层层透传父给子传基础数据(如标题、列表数据、开关状态)
emit子 → 父(单向)子触发事件传递数据,父监听事件处理,子不关心父的逻辑父子(直接)解耦、符合单向数据流跨层级需层层转发子通知父状态变更(如按钮点击、表单输入、弹窗关闭)
ref父 ← 子(父主动访问)父通过 ref 拿到子实例,直接访问 / 调用子的属性 / 方法低(侵入)父子(直接)父可主动操控子组件破坏封装、调试难父需主动控制子行为(如清空输入框、触发子组件刷新)
v-model父 ↔ 子(双向语法糖)本质是 Props(modelValue) + emit(update:modelValue),简化双向绑定父子(直接)代码简洁、语义清晰仅适用于 “值的双向绑定”表单类组件双向绑定(输入框、开关、滑块、下拉框)

provide

/inject

上层 → 下层(跨层级)上层提供数据 / 方法,下层注入使用,跳过中间层级任意层级(跨层)解决多层透传问题数据来源不直观、易滥用跨多层级通信(如主题配置、用户登录状态)、组件库开发
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐