文章目录
一、传值核心认知与场景划分
1.1 组件关系与传值需求
Vue2 组件间关系主要分为三类,对应不同传值策略:
-
父子组件:直接嵌套的层级关系(如页面与子组件)
-
跨级组件:间隔 1 层以上的层级关系(如爷爷与孙子组件)
-
兄弟组件:同一父组件下的平级关系(如表单与列表组件)
1.2 传值核心原则
-
单向数据流:父组件数据更新自动同步到子组件,禁止子组件直接修改 props
-
响应式保证:传值需确保数据响应性,避免原始类型数据无法触发视图更新
-
解耦优先:复杂场景优先使用 Vuex 等工具,避免组件间强依赖
1.3 7 种传值方式概览
| 传值方式 | 适用场景 | 核心优势 | 学习难度 |
|---|---|---|---|
| props/$emit | 父子组件 | 原生支持,简单直观 | ★☆☆☆☆ |
| a t t r s / attrs/ attrs/listeners | 跨级组件 | 无需中间组件转发 | ★★☆☆☆ |
| provide/inject | 跨级组件 | 深层级传值便捷 | ★★☆☆☆ |
| 事件总线 | 任意组件 | 轻量灵活,无层级限制 | ★★☆☆☆ |
| Vuex | 任意组件 | 统一状态管理,适合大型项目 | ★★★☆☆ |
| p a r e n t / parent/ parent/children | 父子组件 | 直接访问组件实例 | ★★☆☆☆ |
| $refs | 父子组件 | 快速获取 DOM 或组件实例 | ★☆☆☆☆ |
二、父子组件传值:props 与 $emit
2.1 父传子:props 详解
基本用法
<!-- 父组件 Parent.vue -->
<template>
<Child
:basic-info="userInfo"
:age="18"
:is-active="true"
@update-name="handleUpdateName"
/>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
data() {
return {
userInfo: { name: 'Alice', gender: 'female' }
}
},
methods: {
handleUpdateName(newName) {
this.userInfo.name = newName
}
}
}
</script>
<!-- 子组件 Child.vue -->
<template>
<div>
<p>姓名:{{ basicInfo.name }}</p>
<p>年龄:{{ age }}</p>
<button @click="changeName">修改姓名</button>
</div>
</template>
<script>
export default {
// props验证(推荐)
props: {
basicInfo: {
type: Object,
required: true,
// 自定义验证
validator: (value) => value.name && value.gender
},
age: {
type: Number,
default: 16,
// 数字类型特殊验证
validator: (value) => value >= 0
},
isActive: {
type: Boolean,
default: false
}
},
methods: {
changeName() {
// 子组件触发父组件方法
this.$emit('update-name', 'Bob')
}
}
}
</script>
关键特性
-
类型支持:String/Number/Boolean/Array/Object/Function/Custom
-
默认值规则:基础类型直接赋值,对象 / 数组需用工厂函数
-
单向数据流:子组件修改 props 需通过 $emit 通知父组件
2.2 子传父:$emit 详解
核心用法
-
触发事件:
this.$emit('事件名', 参数1, 参数2) -
事件验证:Vue2.4 + 支持
emits选项声明事件(类似 props 验证)
// 子组件中声明事件(Vue2.4+)
export default {
emits: {
// 基础声明
'update-name': null,
// 带参数验证
'update-age': (value) => typeof value === 'number'
},
methods: {
sendData() {
this.$emit('update-age', 20) // 验证通过
this.$emit('update-age', '20') // 验证失败(控制台警告)
}
}
}
三、跨级组件传值: a t t r s / attrs/ attrs/listeners 与 provide/inject
3.1 a t t r s 与 attrs与 attrs与listeners
核心原理
-
$attrs:包含父组件传递的非 props 属性(class 和 style 除外)
-
$listeners:包含父组件绑定的所有事件监听器
-
适用场景:中间组件仅做传值转发,自身不使用这些属性 / 事件
实战代码
<!-- 爷爷组件 Grandparent.vue -->
<template>
<Parent
:name="userName"
:age="userAge"
@update-name="handleUpdateName"
/>
</template>
<script>
export default {
data() {
return { userName: 'Alice', userAge: 18 }
},
methods: { handleUpdateName(newName) { this.userName = newName } }
}
</script>
<!-- 父组件 Parent.vue(中间层) -->
<template>
<!-- 转发属性和事件 -->
<Child v-bind="$attrs" v-on="$listeners" />
</template>
<script>
export default {
// 声明的props不会出现在$attrs中
props: ['age'],
created() {
console.log(this.$attrs) // { name: 'Alice' }
console.log(this.$listeners) // { 'update-name': handleUpdateName }
}
}
</script>
<!-- 孙子组件 Child.vue -->
<template>
<div>
<p>姓名:{{ name }}</p>
<button @click="$emit('update-name', 'Bob')">修改姓名</button>
</div>
</template>
<script>
export default {
props: ['name']
}
</script>
3.2 provide 与 inject
核心原理
-
provide:父组件提供数据 / 方法,可跨任意层级传递
-
inject:子组件注入父组件提供的内容
-
默认特性:非响应式,需通过传递响应式对象实现响应性
响应式实现
<!-- 祖先组件 Ancestor.vue -->
<script>
export default {
provide() {
// 传递响应式对象(关键)
return {
user: this.userInfo,
updateUser: this.updateUserInfo
}
},
data() {
return {
userInfo: { name: 'Alice', age: 18 }
}
},
methods: {
updateUserInfo(newData) {
this.userInfo = { ...this.userInfo, ...newData }
}
}
}
</script>
<!-- 深层子组件 DeepChild.vue -->
<script>
export default {
inject: ['user', 'updateUser'],
created() {
console.log(this.user.name) // Alice
},
methods: {
changeAge() {
this.updateUser({ age: 20 }) // 触发祖先组件数据更新
}
}
}
</script>
命名冲突解决方案
使用 Symbol 作为注入键,避免不同组件库的命名冲突:
// src/keys.js
export const USER_KEY = Symbol('user')
export const UPDATE_USER_KEY = Symbol('updateUser')
// 祖先组件
import { USER_KEY, UPDATE_USER_KEY } from '@/keys'
provide() {
return {
[USER_KEY]: this.userInfo,
[UPDATE_USER_KEY]: this.updateUserInfo
}
}
// 子组件
import { USER_KEY, UPDATE_USER_KEY } from '@/keys'
inject: [USER_KEY, UPDATE_USER_KEY]
四、任意组件传值:事件总线与 Vuex
4.1 全局事件总线
实现原理
利用 Vue 实例的事件系统( o n / on/ on/emit/$off)创建全局事件中心,任何组件都可通信。
完整实现
// 1. 创建事件总线(src/utils/eventBus.js)
import Vue from 'vue'
export const EventBus = new Vue()
// 2. 发送事件(组件A)
import { EventBus } from '@/utils/eventBus'
export default {
methods: {
sendMessage() {
// 发送事件并携带参数
EventBus.$emit('user-updated', { name: 'Bob', age: 20 })
// 发送多个参数
EventBus.$emit('show-toast', '操作成功', 'success')
}
}
}
// 3. 监听事件(组件B)
import { EventBus } from '@/utils/eventBus'
export default {
data() { return { userName: '游客' } },
created() {
// 监听事件
this.userHandler = (user) => {
this.userName = user.name
}
this.toastHandler = (text, type) => {
console.log(`[${type}] ${text}`)
}
EventBus.$on('user-updated', this.userHandler)
EventBus.$on('show-toast', this.toastHandler)
},
// 必须解绑事件,避免内存泄漏
beforeDestroy() {
EventBus.$off('user-updated', this.userHandler)
EventBus.$off('show-toast', this.toastHandler)
}
}
4.2 Vuex 状态管理
核心概念
-
State:存储全局状态
-
Mutation:修改状态的唯一途径(同步操作)
-
Action:处理异步操作,通过 commit 调用 mutation
-
Getter:计算派生状态
基础实现
// 1. 初始化Vuex(src/store/index.js)
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
user: { name: 'Alice', age: 18 },
token: ''
},
mutations: {
// 修改用户信息
updateUser(state, newUser) {
state.user = { ...state.user, ...newUser }
},
// 设置token
setToken(state, token) {
state.token = token
}
},
actions: {
// 异步登录(模拟API请求)
login({ commit }, userData) {
return new Promise(resolve => {
setTimeout(() => {
commit('setToken', 'fake-token-123')
commit('updateUser', { name: userData.name })
resolve()
}, 1000)
})
}
},
getters: {
// 派生状态:用户全名(示例)
userFullName(state) {
return `Mr./Ms. ${state.user.name}`
}
}
})
// 2. 注入Vue实例(src/main.js)
import store from './store'
new Vue({
el: '#app',
store,
render: h => h(App)
})
// 3. 组件中使用
export default {
computed: {
// 直接访问state
userName() {
return this.$store.state.user.name
},
// 使用getter
userFullName() {
return this.$store.getters.userFullName
},
// 辅助函数映射(推荐)
...Vuex.mapState(['token']),
...Vuex.mapGetters(['userFullName'])
},
methods: {
// 调用mutation
changeAge() {
this.$store.commit('updateUser', { age: 20 })
},
// 调用action
async handleLogin() {
await this.$store.dispatch('login', { name: 'Bob' })
console.log('登录成功')
},
// 辅助函数映射
...Vuex.mapMutations(['updateUser']),
...Vuex.mapActions(['login'])
}
}
模块化用法(复杂项目)
// src/store/modules/user.js
export default {
namespaced: true, // 开启命名空间
state: { name: 'Alice', age: 18 },
mutations: { updateAge(state, age) { state.age = age } },
actions: { /* 异步操作 */ }
}
// src/store/index.js
import user from './modules/user'
export default new Vuex.Store({
modules: { user }
})
// 组件中使用
computed: {
...Vuex.mapState('user', ['name'])
},
methods: {
...Vuex.mapMutations('user', ['updateAge'])
}
五、特殊传值方式: p a r e n t / parent/ parent/children 与 $refs
5.1 p a r e n t 与 parent与 parent与children
核心用法
-
$parent:子组件访问父组件实例
-
$children:父组件访问子组件实例列表(无序)
<!-- 子组件访问父组件 -->
<script>
export default {
created() {
console.log(this.$parent.userName) // 访问父组件数据
this.$parent.updateUser({ age: 20 }) // 调用父组件方法
}
}
</script>
<!-- 父组件访问子组件 -->
<template>
<div>
<Child ref="child1" />
<Child ref="child2" />
</div>
</template>
<script>
export default {
mounted() {
// $children方式(无序,不推荐)
this.$children.forEach(child => {
console.log(child.name)
})
// $refs方式(推荐,通过ref定位)
this.$refs.child1.changeName()
}
}
</script>
注意事项
-
避免过度依赖:组件耦合度高,不利于维护
-
$children 无顺序:无法保证子组件顺序与模板一致
-
优先使用 props/$emit:仅在特殊场景(如组件库开发)使用
5.2 $refs
核心用法
-
访问 DOM 元素:
<div ref="myDiv"></div>→this.$refs.myDiv -
访问组件实例:
<Child ref="myChild"></Child>→this.$refs.myChild
<template>
<div>
<input ref="usernameInput" type="text">
<Child ref="childComp" />
<button @click="handleClick">操作</button>
</div>
</template>
<script>
export default {
mounted() {
// 访问DOM元素
this.$refs.usernameInput.focus()
},
methods: {
handleClick() {
// 访问组件实例
console.log(this.$refs.childComp.name)
this.$refs.childComp.changeName()
}
}
}
</script>
注意事项
-
初始化时机:mounted 钩子中才能访问到 $refs(DOM 已挂载)
-
避免滥用:优先使用数据驱动,而非直接操作实例
六、实战场景选型与代码示例
6.1 场景 1:父子组件表单传值
<!-- 父组件 FormPage.vue -->
<template>
<div>
<FormInput
label="用户名"
v-model="formData.username"
:required="true"
/>
<FormInput
label="密码"
v-model="formData.password"
type="password"
/>
<button @click="submitForm">提交</button>
</div>
</template>
<script>
export default {
data() {
return { formData: { username: '', password: '' } }
},
methods: { submitForm() { console.log(this.formData) } }
}
</script>
<!-- 子组件 FormInput.vue -->
<template>
<div class="form-item">
<label>{{ label }}:</label>
<input
:type="type"
:value="value"
@input="$emit('input', $event.target.value)"
:required="required"
>
</div>
</template>
<script>
export default {
props: {
label: String,
value: String,
type: { type: String, default: 'text' },
required: Boolean
}
}
</script>
6.2 场景 2:兄弟组件状态同步
// 方案1:事件总线实现
// 组件A(发送方)
import { EventBus } from '@/utils/eventBus'
export default {
methods: {
sendData() {
EventBus.$emit('data-updated', { id: 1, name: '商品A' })
}
}
}
// 组件B(接收方)
import { EventBus } from '@/utils/eventBus'
export default {
data() { return { product: null } },
created() {
EventBus.$on('data-updated', (data) => {
this.product = data
})
},
beforeDestroy() {
EventBus.$off('data-updated')
}
}
// 方案2:Vuex实现(推荐)
// store/modules/product.js
export default {
state: { current: null },
mutations: { setCurrent(state, product) { state.current = product } }
}
// 组件A
this.$store.commit('product/setCurrent', product)
// 组件B
computed: {
currentProduct() {
return this.$store.state.product.current
}
}
6.3 场景 3:深层级组件主题配置
<!-- 根组件 App.vue -->
<script>
export default {
provide() {
return {
theme: this.theme,
toggleTheme: this.toggleTheme
}
},
data() { return { theme: 'light' } },
methods: {
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light'
}
}
}
</script>
<!-- 深层子组件 ThemeButton.vue -->
<template>
<button :class="theme" @click="toggleTheme">
切换主题
</button>
</template>
<script>
export default {
inject: ['theme', 'toggleTheme'],
computed: {
buttonClass() {
return `btn-${this.theme}`
}
}
}
</script>
七、常见问题与避坑指南
7.1 经典问题解析
| 问题场景 | 原因分析 | 解决方案 |
|---|---|---|
| props 修改不生效 | 1. 子组件直接修改 props2. 父组件传递原始类型数据后重新赋值 | 1. 子组件通过 $emit 通知父组件修改2. 父组件传递对象 / 数组(引用类型) |
| 事件总线多次触发 | 组件重复挂载未解绑事件 | 1. beforeDestroy 中 o f f 解绑 < b r > 2. 使用 off解绑<br>2. 使用 off解绑<br>2.使用once 监听一次性事件 |
| provide/inject 数据不响应 | 传递的是基础类型数据(非响应式) | 传递响应式对象:provide () { return { data: this.obj} } |
| Vuex 状态更新视图不刷新 | 1. 直接修改 state 而非通过 mutation2. 添加新属性未使用 Vue.set | 1. 严格模式下禁止直接修改 state2. 使用 Vue.set (obj, key, value) 或展开运算符 |
| $refs 获取不到组件 | 1. 组件未挂载完成2. 条件渲染未满足 | 1. 在 mounted 钩子中访问2. 确保条件渲染为 true 时访问 |
7.2 传值方式选型指南
-
简单父子组件:props/$emit(首选)
-
表单组件通信:v-model(语法糖,本质是 props+$emit)
-
深层级组件:provide/inject(组件库开发)、Vuex(业务逻辑)
-
兄弟组件:事件总线(小型项目)、Vuex(大型项目)
-
DOM 操作需求:$refs(仅在必要时使用)
-
中间组件透传: a t t r s / attrs/ attrs/listeners(减少代码冗余)
7.3 性能优化建议
-
避免过度使用 Vuex:小型项目用事件总线即可,减少性能开销
-
props 按需传递:避免传递不必要的属性,减少组件重渲染
-
事件及时解绑:事件总线和 $on 监听必须在组件销毁时解绑
-
Vuex 状态拆分:大型项目使用 modules 拆分状态,提高维护性
-
计算属性缓存:使用 computed 访问 Vuex 状态,避免重复计算

584

被折叠的 条评论
为什么被折叠?



