一文看懂Vue2 与 Vue3 组件传参

51 阅读2分钟

Vue2 组件通信方式

  1. Props(父传子)
<!-- 父组件 Parent.vue -->
<template>
  <Child :title="parentTitle" :user="userData" />
</template>

<script>
import Child from './Child.vue'
export default {
  components: { Child },
  data() {
    return {
      parentTitle: 'Hello from Parent',
      userData: { name: 'John', age: 25 }
    }
  }
}
</script>

<!-- 子组件 Child.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <p>Name: {{ user.name }}</p >
  </div>
</template>

<script>
export default {
  props: {
    // 基础类型
    title: {
      type: String,
      required: true,
      default: 'Default Title'
    },
    // 对象类型
    user: {
      type: Object,
      default: () => ({})
    }
  }
}
</script>
  1. $emit(子传父)
<!-- 子组件 Child.vue -->
<template>
  <button @click="sendMessage">发送消息给父组件</button>
</template>

<script>
export default {
  methods: {
    sendMessage() {
      this.$emit('message-from-child', 'Hello Parent!')
    }
  }
}
</script>

<!-- 父组件 Parent.vue -->
<template>
  <Child @message-from-child="handleChildMessage" />
</template>

<script>
export default {
  methods: {
    handleChildMessage(msg) {
      console.log('收到子组件消息:', msg)
    }
  }
}
</script>
  1. Event Bus(全局事件总线)
// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()

// 组件A - 发送事件
<script>
import { EventBus } from './eventBus'
export default {
  methods: {
    sendData() {
      EventBus.$emit('global-event', { data: 'some data' })
    }
  }
}
</script>

// 组件B - 接收事件
<script>
import { EventBus } from './eventBus'
export default {
  created() {
    EventBus.$on('global-event', (payload) => {
      console.log('收到事件:', payload)
    })
  },
  beforeDestroy() {
    EventBus.$off('global-event')
  }
}
</script>
  1. Vuex(状态管理)
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state, payload) {
      state.count += payload.amount
    }
  },
  actions: {
    incrementAsync({ commit }, payload) {
      setTimeout(() => {
        commit('increment', payload)
      }, 1000)
    }
  }
})

// 组件中使用
<script>
export default {
  computed: {
    count() {
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment', { amount: 1 })
      // 或使用actions
      this.$store.dispatch('incrementAsync', { amount: 1 })
    }
  }
}
</script>
  1. $attrs$listeners
<!-- 父组件 Parent.vue -->
<template>
  <Child 
    title="传递的标题"
    :data="someData"
    @custom-event="handleEvent"
  />
</template>

<!-- 中间组件 Child.vue -->
<template>
  <!-- 传递所有属性和事件 -->
  <GrandChild v-bind="$attrs" v-on="$listeners" />
</template>

<script>
export default {
  inheritAttrs: false // 不将attrs设置为DOM属性
}
</script>

<!-- 最终组件 GrandChild.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <button @click="$emit('custom-event')">触发事件</button>
  </div>
</template>

<script>
export default {
  props: ['title']
}
</script>

Vue3 组件通信方式

  1. Props(父传子) - Composition API
<!-- 父组件 Parent.vue -->
<template>
  <Child 
    :title="parentTitle" 
    :user="userData"
    @update:title="parentTitle = $event"
  />
</template>

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

const parentTitle = ref('Hello from Parent')
const userData = ref({ name: 'John', age: 25 })
</script>

<!-- 子组件 Child.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <button @click="updateTitle">修改标题</button>
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  title: {
    type: String,
    required: true,
    default: 'Default Title'
  },
  user: {
    type: Object,
    default: () => ({})
  }
})

const emit = defineEmits(['update:title'])

const updateTitle = () => {
  emit('update:title', 'New Title from Child')
}
</script>
  1. defineEmits(子传父)
<!-- 子组件 Child.vue -->
<template>
  <button @click="sendData">发送数据</button>
</template>

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

const emit = defineEmits({
  // 带验证的emit
  'send-message': (payload) => {
    return typeof payload === 'string'
  }
})

const sendData = () => {
  emit('send-message', 'Hello from Child')
  emit('update:count', 10) // 支持v-model语法
}
</script>

<!-- 父组件 Parent.vue -->
<template>
  <Child 
    @send-message="handleMessage"
    @update:count="count = $event"
  />
</template>

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

const count = ref(0)

const handleMessage = (msg) => {
  console.log('收到:', msg)
}
</script>
  1. provide/inject(跨层级通信)
<!-- 祖先组件 Ancestor.vue -->
<template>
  <Parent />
</template>

<script setup>
import { provide, ref } from 'vue'
import Parent from './Parent.vue'

const sharedData = ref('共享数据')
const updateSharedData = (newValue) => {
  sharedData.value = newValue
}

// 提供数据和方法
provide('sharedData', {
  sharedData,
  updateSharedData
})
</script>

<!-- 后代组件 Descendant.vue -->
<template>
  <div>{{ data.sharedData }}</div>
</template>

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

const data = inject('sharedData')

const updateData = () => {
  data.updateSharedData('新的共享数据')
}
</script>
  1. Pinia(Vue3推荐的状态管理)
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment(amount = 1) {
      this.count += amount
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2
  }
})

// 组件中使用
<template>
  <div>{{ store.count }}</div>
  <div>双倍: {{ store.doubleCount }}</div>
  <button @click="store.increment()">增加</button>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()
</script>
  1. 模板引用和 defineExpose
<!-- 父组件 Parent.vue -->
<template>
  <Child ref="childRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>

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

const childRef = ref(null)

const callChildMethod = () => {
  childRef.value?.childMethod()
  console.log('子组件数据:', childRef.value?.childData)
}
</script>

<!-- 子组件 Child.vue -->
<template>
  <div>子组件</div>
</template>

<script setup>
import { ref, defineExpose } from 'vue'

const childData = ref('子组件数据')

const childMethod = () => {
  console.log('子组件方法被调用')
}

// 暴露给父组件
defineExpose({
  childData,
  childMethod
})
</script>
  1. 自定义Hooks(组合式函数)
// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const increment = () => count.value++
  const decrement = () => count.value--
  const double = computed(() => count.value * 2)
  
  return {
    count,
    increment,
    decrement,
    double
  }
}

// 组件中使用
<script setup>
import { useCounter } from '@/composables/useCounter'

const { count, increment, double } = useCounter(10)
</script>

Vue2 vs Vue3 通信方式对比

特性Vue2Vue3
Props声明props: {} 选项defineProps() 组合式API
事件发射this.$emit()defineEmits()
跨组件通信provide/inject 选项provide()/inject() 函数
状态管理VuexPinia(推荐)或 Vuex 4
暴露组件方法自动暴露public方法defineExpose() 显式暴露
属性透传attrsattrs 和 listenersuseAttrs() 和 useListeners()
响应式数据data() 返回对象ref() 或 reactive()
全局事件总线需要创建Vue实例使用Mitt等第三方库

最佳实践建议

Vue2项目:

  • 简单父子通信使用 props/$emit
  • 复杂状态管理使用 Vuex
  • 跨层级组件使用 provide/inject
  • 事件总线适合简单场景,注意及时清理

Vue3项目:

  • 优先使用组合式API
  • 状态管理推荐 Pinia
  • 使用TypeScript增强类型安全
  • 复杂逻辑抽离为组合式函数
  • 注意响应式数据的正确使用