vue2 - Day03 - (生命周期、组件、组件通信)
文章目录
- 一、生命周期
- 1. 创建阶段
- 2. 挂载阶段
- 3. 更新阶段
- 4. 销毁阶段
- 5. 错误捕获
- 总结
- 二、组件
- 2.1 注册
- 1. 全局注册 - 公共的组件。
- 2. 局部注册
- 总结
- 2.2 三大重要的组成部分
- 1. 模板 (Template)
- 主要功能:
- 说明:
- 2. 脚本 (Script)
- 主要功能:
- 说明:
- 3. 样式 (Style)
- 主要功能:
- 说明:
- 三个部分的关系
- 解析:
- 总结
- 2.3 scoped原理
- `scoped` 的原理
- 编译后的代码:
- 为什么 `scoped` 能起作用?
- `scoped` 样式的限制
- `scoped` 子组件样式穿透
- 父组件:
- 子组件(ChildComponent.vue):
- 总结
- 2.4 在 Vue 2 中,`data` 是一个函数而不是一个对象。
- 为什么 `data` 是一个函数?
- 1. Vue 组件是可复用的
- 2. 返回新的数据对象
- 为什么 `data` 需要是一个函数
- 例子
- 解释:
- `data` 作为函数的好处
- 为什么不能直接使用对象?
- 总结
- 三、组件通信
- 3.1 父子组件通信
- 父组件传递数据给子组件 (Props)
- 子组件向父组件传递数据
- 说明:
- 3.2 兄弟组件通信
- 父组件作为中介
- 3.3 跨层级组件通信
- (1) Event Bus (事件总线)
- (2) Vuex (全局状态管理)
- 3.4 总结
- 3.5 props 校验
- `props` 校验的基础语法
- 常见的 `props` 校验规则
- 组合校验规则
- 校验规则的执行时机
- 代码
- 总结
- 3.6 `props` 和 `data`:单向数据流
- 1. `props` 和 `data` 的区别
- (1) `props`
- (2) `data`
- 2. 单向数据流的概念
- 3. 如何遵循单向数据流
- (1) 父组件向子组件传递数据 (Props)
- (2) 子组件通过事件通知父组件 (Event Emitting)
- 4. 为什么需要单向数据流?
- (1) 数据追踪更简单
- (2) 调试和维护更容易
- (3) 减少了副作用
- (4) 便于构建组件库
- 5. Vuex:管理复杂应用的单向数据流
- 总结
- 四、非父子组件通信
- 通过 `provide` 和 `inject` 进行跨层级通信
- 1. `provide` 和 `inject` 的基本原理
- 2. 使用场景
- 3. 基本用法
- 3.1 祖先组件使用 `provide` 提供数据
- 3.2 后代组件使用 `inject` 接收数据
- 4. 工作原理
- 5. 举例说明
- 5.1 跨层级数据共享
- 5.2 跨层级函数共享
- 6. `provide` 和 `inject` 的注意事项
- 7. 总结
一、生命周期
1. 创建阶段
-
beforeCreate
该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务器端进行。
beforeCreate() { console.log('beforeCreate'); }
-
created
在实例创建完成后立即调用,数据观测和事件配置都已经完成。这时可以访问到data
、computed
、methods
等数据和方法,但尚未挂载 DOM。created() { console.log('created'); }
2. 挂载阶段
-
beforeMount
在挂载开始之前被调用,相关的render
函数首次被调用,DOM 尚未渲染。beforeMount() { console.log('beforeMount'); }
-
mounted
在挂载完成后立即调用,el
被替换成 DOM 后可以访问到 DOM 元素。mounted() { console.log('mounted'); }
3. 更新阶段
-
beforeUpdate
当数据更新且视图重新渲染之前被调用。在这里访问更新前的 DOM 状态。beforeUpdate() { console.log('beforeUpdate'); }
-
updated
数据更新且视图重新渲染后调用。此时可以访问到更新后的 DOM。updated() { console.log('updated'); }
4. 销毁阶段
-
beforeDestroy
在实例销毁之前调用。此时组件的 DOM 和数据绑定尚未清理,可以在此阶段做一些清理工作,比如移除事件监听器等。beforeDestroy() { console.log('beforeDestroy'); }
-
destroyed
实例销毁之后调用。组件的所有绑定和事件监听都被移除,所有的子组件也都被销毁。destroyed() { console.log('destroyed'); }
5. 错误捕获
-
errorCaptured
当子组件的错误被捕获时调用。该钩子接收三个参数:错误信息 (err
)、错误来源组件 (vm
)、错误所在的生命周期钩子 (info
)。errorCaptured(err, vm, info) { console.error('Error captured:', err); return false; // 如果返回 false,错误不会再向上传播 }
总结
Vue 的生命周期提供了钩子函数,使得你可以在合适的时机进行某些操作。不同阶段的生命周期钩子适用于不同的场景,比如在 created
中获取数据、在 mounted
中设置 DOM 操作、在 beforeDestroy
中进行清理等。
二、组件
在 Vue 2 中,根组件(Root Component)是整个 Vue 应用的入口组件,它是 Vue 实例的最顶层组件,所有的其他组件都是在根组件的基础上注册和嵌套的。根组件通常负责整个应用的初始化、配置和全局数据管理。
- 根组件是 Vue 应用的入口:在一个 Vue 应用中,只有一个根组件,它是整个应用的最外层组件。所有的子组件都是由根组件管理和调度的。
- Vue 实例挂载在根组件上:Vue 的应用实例是挂载到根组件上的,根组件通过 el 将 Vue 实例挂载到页面的 DOM 元素上。
- 根组件通常包含其他子组件:根组件会注册其他组件,并将它们通过模板语法嵌套在自身的模板中。
2.1 注册
在 Vue 2 中,组件的注册可以分为 **局部注册** 和 **全局注册** 两种方式。
1. 全局注册 - 公共的组件。
全局注册的步骤:
- 在
main.js
或app.js
中导入组件。 - 使用
Vue.component()
方法进行注册。
// main.js
import Vue from 'vue'
import App from './App.vue'
import MyComponent from './components/MyComponent.vue'
// 全局注册组件
Vue.component('my-component', MyComponent)
new Vue({
render: h => h(App),
}).$mount('#app')
这样,MyComponent
组件就可以在应用的任何地方使用了,使用时直接通过 <my-component></my-component>
引用。
2. 局部注册
局部注册的组件只能在其所在的父组件中使用。
只在某个特定组件内使用的组件。
局部注册的步骤:
- 在父组件中导入子组件。
- 在父组件的
components
选项中注册该组件。
// ParentComponent.vue
<template>
<div>
<my-component></my-component>
</div>
</template>
<script>
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent // 局部注册
}
}
</script>
这样,MyComponent
只会在 ParentComponent
组件中可用,而不能在其他地方直接使用。
总结
- 全局注册:通过
Vue.component()
在main.js
中注册,组件可以在整个应用中使用。 - 局部注册:在父组件中通过
components
选项注册,组件只在父组件中有效。
通常来说,如果某个组件只在少数几个地方使用,建议使用 局部注册,以便减少命名冲突和提升代码的可维护性。全局注册适用于通用的、需要在多个地方使用的组件,如 Button
, Modal
等。
2.2 三大重要的组成部分
在 Vue 2 中,组件主要由 模板 (Template)、脚本 (Script) 和 样式 (Style) 即:视图、逻辑和样式。
1. 模板 (Template)
模板是 Vue 组件的视图部分,负责定义组件的 HTML 结构。Vue 使用声明式渲染方式来绑定数据和 DOM,模板部分包含了 HTML 语法和 Vue 特有的指令(如 v-bind
, v-if
, v-for
等)。它不仅能展示数据,还能响应用户的交互。
主要功能:
- 动态渲染数据:模板能够渲染动态数据,通过插值语法(
{{ }}
)或者指令来绑定数据到 DOM。 - 事件处理:使用
v-on
指令或@
简写来绑定事件,处理用户的交互(如点击、输入等)。 - 条件渲染和循环渲染:使用
v-if
,v-for
,v-show
等指令来控制 DOM 的渲染和显示。
<template>
<div>
<h1>{{ title }}</h1> <!-- 数据绑定 -->
<button @click="changeMessage">Change Message</button> <!-- 事件绑定 -->
<!-- 条件渲染 -->
<p v-if="isVisible">This is visible if condition is true.</p>
<!-- 列表渲染 -->
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
说明:
{{ title }}
:插值表达式,用于将数据绑定到 DOM 元素。@click="changeMessage"
:事件绑定,绑定按钮的点击事件,触发changeMessage
方法。v-if
:条件渲染,只有isVisible
为true
时,<p>
标签才会渲染。v-for
:循环渲染,遍历items
数组,并为每个item
渲染一个<li>
元素。
2. 脚本 (Script)
脚本部分包含 Vue 组件的业务逻辑,包括数据、方法、计算属性、生命周期钩子等。通过 export default
定义一个 Vue 组件对象,Vue 会将这些数据和方法与模板部分绑定。
主要功能:
- 数据 (
data
):定义组件的响应式数据。 - 方法 (
methods
):定义组件的行为或逻辑,用户的点击事件。 - 计算属性 (
computed
):计算基于响应式数据的派生状态,常用于替代方法来优化性能。 - 生命周期钩子:例如
created
,mounted
,updated
,destroyed
等,在组件不同生命周期阶段执行代码。
<script>
export default {
data() {
return {
title: 'Hello, Vue!',
isVisible: true,
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]
};
},
methods: {
changeMessage() {
this.title = 'Message has been changed!';
}
},
computed: {
itemCount() {
return this.items.length;
}
},
mounted() {
console.log('Component is mounted!');
}
}
</script>
说明:
data()
:返回组件的响应式数据,这些数据可以在模板中使用。methods
:包含可以在模板中调用的方法(如changeMessage
)。computed
:计算属性itemCount
,它基于items
数组的长度返回一个值,computed
会缓存计算结果,只有items
发生变化时才会重新计算。mounted()
:生命周期钩子函数,组件挂载到 DOM 后会执行该方法。
3. 样式 (Style)
样式部分用于定义组件的视觉效果和布局。Vue 允许你在组件中直接写 CSS 样式,也支持预处理器如 SASS/SCSS 和 LESS 等。Vue 组件的样式有两种应用方式:全局样式和局部样式。
主要功能:
- 局部样式:通过在
<style>
标签上添加scoped
属性,使样式只作用于当前组件,避免影响到全局其他组件。 - 全局样式:如果不使用
scoped
属性,样式将影响到全局的其他组件。 - 使用预处理器:可以在
<style>
标签中使用 SASS/SCSS、LESS 等预处理器,提升样式的灵活性和可维护性。
<style scoped>
h1 {
color: blue;
}
button {
background-color: lightgray;
border: none;
padding: 10px;
}
</style>
说明:
scoped
:局部样式,只会作用于当前组件的元素,不会影响到其他组件。background-color: lightgray;
:样式定义了按钮的背景颜色。
三个部分的关系
- 模板:负责定义组件的视图结构。
- 脚本:定义组件的行为、数据和生命周期,提供逻辑处理。
- 样式:定义组件的样式和外观,控制组件的视觉效果。
<template>
<div>
<h1>{{ title }}</h1>
<button @click="toggleVisibility">Toggle Visibility</button>
<p v-if="isVisible">This paragraph will be toggled on click.</p>
</div>
</template>
<script>
export default {
data() {
return {
title: 'Vue Component Example',
isVisible: true
};
},
methods: {
toggleVisibility() {
this.isVisible = !this.isVisible;
}
}
}
</script>
<style scoped>
h1 {
color: green;
}
button {
background-color: #42b983;
color: white;
border: none;
padding: 10px;
cursor: pointer;
}
button:hover {
background-color: #35495e;
}
</style>
解析:
- 模板部分:展示了一个标题、按钮和一个可切换显示的段落。
- 脚本部分:定义了
title
和isVisible
数据,提供了toggleVisibility
方法来切换isVisible
的值。 - 样式部分:为标题和按钮添加了样式,使其具有更好的视觉效果。
总结
- 模板 (Template):定义 HTML 结构,使用 Vue 特有的指令来动态渲染视图。
- 脚本 (Script):定义组件的行为,处理数据、方法和生命周期,提供组件的逻辑。
- 样式 (Style):定义组件的样式,支持局部样式和预处理器。
2.3 scoped原理
在 Vue 组件中,scoped
是一个 <style>
标签的特殊属性,它用来实现 局部样式。当在 <style>
标签中使用 scoped
属性时,样式只会作用于当前组件的元素,而不会影响到全局的其他组件或页面的元素。这样可以避免不同组件之间的样式冲突,确保组件的样式是隔离的。
scoped
的原理
scoped
的工作原理依赖于 Vue 的 CSS作用域隔离。它通过 自动添加样式选择器的唯一属性 来实现样式的局部化。
-
样式选择器加上独特的属性:
当你在 Vue 组件的<style>
标签上使用scoped
时,Vue 会在编译期间,给该组件的每个样式选择器添加一个独特的属性(通常是基于组件的唯一 ID),从而使这些样式只作用于该组件内的元素。 -
自动生成唯一的标识符:
Vue 会为每个组件生成一个唯一的标识符(比如data-v-xxxxxxx
),并将这个标识符作为选择器的一部分添加到每个 CSS 规则中。 -
修改生成的 HTML 元素:
Vue 会为组件中的 HTML 元素添加与样式选择器相匹配的属性(如data-v-xxxxxxx
)。这些属性帮助样式选择器只作用于当前组件的 DOM 元素。
<template>
<div class="example">
<p>This is a scoped example!</p>
</div>
</template>
<script>
export default {
name: 'ScopedComponent'
}
</script>
<style scoped>
.example {
color: red;
}
</style>
编译后的代码:
<template>
<div class="example" data-v-1234567>
<p data-v-1234567>This is a scoped example!</p>
</div>
</template>
<script>
export default {
name: 'ScopedComponent'
}
</script>
<style scoped>
.example[data-v-1234567] {
color: red;
}
</style>
scoped
会让 Vue 自动为 .example
和 <p>
元素添加一个 data-v-1234567
属性。该属性的值是由 Vue 自动生成的唯一标识符,用于确保样式只作用于当前组件中的元素。最终生成的 CSS 规则变为 .example[data-v-1234567]
,因此只有拥有 data-v-1234567
的元素会应用该样式。
为什么 scoped
能起作用?
- 样式选择器的作用范围变小:在样式选择器中加入
data-v-xxxxxxx
这种唯一标识符时,CSS 的作用范围变得非常精确,确保样式只会作用于具有相同标识符的 DOM 元素。 - 生成的标识符是独立的:由于 Vue 为每个组件生成独立的标识符,确保了样式不会与其他组件的样式发生冲突。例如,两个组件中的
.example
类会被编译成不同的选择器,避免了样式覆盖问题。
scoped
样式的限制
虽然 scoped
样式可以隔离组件的样式,但它并不是万能的,也有一些限制和注意事项:
-
不能作用于全局样式:
scoped
样式只能影响当前组件内的元素,不能影响到外部组件或页面的全局元素。当某些样式作用于全局,可以使用普通的<style>
标签,或者通过 CSS 类全局引入样式。 -
伪类和伪元素的特殊情况:
scoped
不适用于伪类(如:hover
)和伪元素(如::after
)。不过,这通常并不是一个问题,因为 Vue 会自动将伪类和伪元素与组件的作用域一起处理。 -
深度选择器
::v-deep
:
需要在组件的样式中影响子组件的样式(例如样式穿透),可以使用::v-deep
选择器。这个选择器会将样式应用到子组件的元素中,而不受scoped
限制。<style scoped> ::v-deep .child-class { color: blue; } </style>
-
不能影响外部的全局 CSS 文件:
当在外部引用了全局样式文件,scoped
不能对它们进行局部化控制。如果需要针对某个组件内的元素应用全局样式,最好将样式放在组件外部的<style>
标签或 CSS 文件中。
scoped
子组件样式穿透
假设有一个父组件和一个子组件,当父组件的样式可以影响到子组件的内部样式。可以使用 ::v-deep
来实现样式的穿透。
父组件:
<template>
<div>
<child-component></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
}
}
</script>
<style scoped>
/* 使用 ::v-deep 让父组件的样式影响到子组件 */
::v-deep .child-class {
color: red;
}
</style>
(1)或者使用>>>
(2) /deep/
子组件(ChildComponent.vue):
<template>
<div class="child-class">
xx
</div>
</template>
<script>
export default {
name: 'ChildComponent'
}
</script>
<style scoped>
/* 这个样式只会应用在子组件中 */
.child-class {
font-size: 20px;
}
</style>
::v-deep
使得父组件的样式能够影响到子组件中 .child-class
类的文本颜色。
总结
- 原理:
scoped
通过为每个组件生成唯一的标识符,并将该标识符添加到样式选择器和 DOM 元素中,从而确保样式只作用于当前组件的元素。 - 优点:避免了不同组件之间样式的冲突,实现了组件化的样式隔离。
- 局限性:不能影响全局样式,不能作用于伪类/伪元素,也不能穿透子组件的样式,除非使用
::v-deep
。
scoped
是 Vue 中非常强大的功能,它帮助开发者在多个组件之间实现样式的局部化,减少了 CSS 冲突的风险。
2.4 在 Vue 2 中,data
是一个函数而不是一个对象。
为什么 data
是一个函数?
1. Vue 组件是可复用的
Vue 组件是可以多次实例化的,且每个实例都有自己的独立状态。由于 Vue 是基于组件化的结构,它允许多个组件实例存在,而每个实例都应该有自己独立的 data
数据。将 data
定义为一个函数而非直接对象,确保了每个组件实例都有自己的独立数据副本。
- 如果
data
是对象的话,所有的组件实例将共享同一份数据,这样就无法保证每个实例的独立性。 - 将
data
定义为函数可以避免这个问题。每次创建新的组件实例时,data
函数都会被调用,从而返回一个新的对象,确保每个组件实例都有自己的数据副本。
2. 返回新的数据对象
在 Vue 组件中,data
是一个函数,它返回一个对象。这个对象包含了该组件的所有响应式数据。这是为了保证每个组件实例都有自己独立的 data
,避免多个实例共享同一份数据。
为什么 data
需要是一个函数
// 错误的写法
Vue.component('my-component', {
data: {
message: 'Hello, World!'
}
});
在上面的代码中,data
直接是一个对象。问题是,如果多个组件实例使用这个组件,它们将共享同一个 data
对象,所有实例的数据都将相互影响。
正确的写法是将 data
作为一个函数:
// 正确的写法
Vue.component('my-component', {
data() {
return {
message: 'Hello, World!'
};
}
});
在这个正确的写法中,data
是一个返回对象的函数。每次创建组件实例时,Vue 会调用 data()
函数,返回一个新的对象,从而确保每个组件实例拥有自己的独立数据副本。
例子
<template>
<div>
<p>{{ message }}</p>
<button @click="changeMessage">Change Message</button>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello, Vue!'
};
},
methods: {
changeMessage() {
this.message = 'Message has been changed!';
}
}
}
</script>
解释:
data()
是一个函数:每个组件实例都会调用data()
方法,返回一个包含message
的对象。每个组件实例有自己的message
数据,不会相互影响。- 数据的响应性:
message
是一个响应式数据,组件会自动监听它的变化,当调用changeMessage
方法时,message
被更新,视图也会随之更新。
data
作为函数的好处
-
避免数据共享:Vue 会为每个组件实例调用
data()
函数,确保每个实例都有一个独立的data
对象。 -
支持多次实例化:多个组件实例可以独立使用自己的数据副本,而不会互相干扰。
-
响应式系统的运作:Vue 会将
data()
返回的对象变成响应式数据,确保视图能够自动更新。
为什么不能直接使用对象?
如果 data
是一个对象而不是一个函数,Vue 会把这个对象直接作为组件实例的共享数据。这样,在多个组件实例中,这个共享对象就会在各个实例之间共享,导致数据污染或意外的相互干扰。
例如,以下代码是错误的:
Vue.component('my-component', {
data: {
message: 'Hello, Vue!'
}
});
这会导致所有使用该组件的实例共享同一个 data
对象,因此一个组件实例中数据的改变会影响到其他实例。为了避免这种情况,Vue 设计上选择将 data
设置为一个函数,使得每个组件实例都能拥有独立的 data
。
总结
data
是一个函数而非对象,这是因为每个 Vue 组件实例应该拥有自己独立的状态。如果data
是对象,多个组件实例将共享同一个数据对象,导致数据冲突。data()
是为了确保每个组件实例都有自己独立的数据副本,这样可以避免数据共享和污染的问题。data()
返回的对象是响应式的,Vue 会追踪其变化并自动更新视图。
通过这种方式,Vue 实现了组件的数据隔离和响应式数据更新机制,从而增强了组件的可复用性和数据的独立性。
三、组件通信
在 Vue 2 中,组件之间的通信是 Vue 应用中非常重要的部分。不同组件之间的通信方式可以根据组件的关系(如父子组件、兄弟组件或跨层级组件)而有所不同。
3.1 父子组件通信
父组件传递数据给子组件 (Props)
在 Vue 中,父组件可以通过 props 向子组件传递数据。props
是一种从父组件向子组件传递数据的方式,它是单向数据流的体现,即父组件的数据可以通过 props
传递给子组件,但子组件不能直接修改父组件的数据。
父组件传递数据给子组件
<!-- ParentComponent.vue -->
<template>
<div>
<child-component :message="parentMessage"></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
parentMessage: 'Hello from Parent!'
};
}
}
</script>
<!-- ChildComponent.vue -->
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
props: ['message']
}
</script>
说明:
- 在父组件中,使用
:message="parentMessage"
传递了数据。- 在子组件中,通过
props
接收父组件传递的数据并渲染。
子组件向父组件传递数据
子组件向父组件传递数据,通常是通过自定义事件实现的。子组件通过 $emit
向父组件发送事件,父组件监听这个事件并执行相应的操作。
子组件向父组件传递数据
<!-- ParentComponent.vue -->
<template>
<div>
<child-component @message-changed="handleMessageChanged"></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
methods: {
handleMessageChanged(newMessage) {
console.log('Message from child:', newMessage);
}
}
}
</script>
<!-- ChildComponent.vue -->
<template>
<div>
<button @click="sendMessageToParent">Send Message</button>
</div>
</template>
<script>
export default {
methods: {
sendMessageToParent() {
this.$emit('message-changed', 'Hello from Child!');
}
}
}
</script>
说明:
- 子组件通过
this.$emit('message-changed', 'Hello from Child!')
向父组件发送message-changed
事件并附带数据。 - 父组件通过
@message-changed="handleMessageChanged"
监听这个事件,并处理来自子组件的数据。
3.2 兄弟组件通信
兄弟组件间的通信可以通过 父组件 进行中介(即,父组件充当传递数据的桥梁)。兄弟组件之间无法直接互相通信,因此可以通过父组件将数据从一个兄弟组件传递到另一个。
父组件作为中介
<!-- ParentComponent.vue -->
<template>
<div>
<sibling-one @message-changed="updateMessage"></sibling-one>
<sibling-two :message="message"></sibling-two>
</div>
</template>
<script>
import SiblingOne from './SiblingOne.vue';
import SiblingTwo from './SiblingTwo.vue';
export default {
components: {
SiblingOne,
SiblingTwo
},
data() {
return {
message: ''
};
},
methods: {
updateMessage(newMessage) {
this.message = newMessage;
}
}
}
</script>
<!-- SiblingOne.vue -->
<template>
<div>
<button @click="sendMessage">Send Message to Sibling 2</button>
</div>
</template>
<script>
export default {
methods: {
sendMessage() {
this.$emit('message-changed', 'Message from Sibling One');
}
}
}
</script>
<!-- SiblingTwo.vue -->
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
props: ['message']
}
</script>
说明:
SiblingOne
通过$emit
向父组件发送事件,父组件接收这个事件并更新message
。SiblingTwo
接收父组件传递的message
,并将其显示出来。
3.3 跨层级组件通信
跨层级组件通信可以通过 Event Bus 或 Vuex 实现。
(1) Event Bus (事件总线)
Event Bus 是通过一个中央的 Vue 实例作为消息中心,允许多个组件之间通过发布订阅模式进行通信。事件总线常用于兄弟组件、父子组件等之间的通信。
Event Bus 实现跨层级通信
// eventBus.js
import Vue from 'vue';
export const EventBus = new Vue();
<!-- SenderComponent.vue -->
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script>
import { EventBus } from './eventBus';
export default {
methods: {
sendMessage() {
EventBus.$emit('message-sent', 'Hello from Sender!');
}
}
}
</script>
<!-- ReceiverComponent.vue -->
<template>
<div>{{ receivedMessage }}</div>
</template>
<script>
import { EventBus } from './eventBus';
export default {
data() {
return {
receivedMessage: ''
};
},
created() {
EventBus.$on('message-sent', (message) => {
this.receivedMessage = message;
});
},
destroyed() {
EventBus.$off('message-sent'); // 清理事件监听器
}
}
</script>
说明:
SenderComponent
通过EventBus.$emit
发送事件。ReceiverComponent
通过EventBus.$on
监听该事件并接收消息。
(2) Vuex (全局状态管理)
Vuex 是 Vue 的官方状态管理库,用于在大型应用中管理和共享组件之间的状态。Vuex 适用于组件间需要共享数据,且这个数据需要跨越多个层级的场景。
Vuex 基础使用:
-
安装 Vuex:
npm install vuex
-
配置 Vuex 存储:
// store.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export const store = new Vuex.Store({ state: { message: '' }, mutations: { setMessage(state, message) { state.message = message; } }, actions: { updateMessage({ commit }, message) { commit('setMessage', message); } } });
-
在组件中使用 Vuex:
<!-- SenderComponent.vue -->
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script>
export default {
methods: {
sendMessage() {
this.$store.dispatch('updateMessage', 'Hello from Sender!');
}
}
}
</script>
<!-- ReceiverComponent.vue -->
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
computed: {
message() {
return this.$store.state.message;
}
}
}
</script>
说明:
- 使用 Vuex 管理状态,在
SenderComponent
组件中更新状态,在ReceiverComponent
组件中读取共享的状态。
3.4 总结
-
父子组件通信:
- 父组件通过
props
向子组件传递数据。 - 子组件通过
$emit
向父组件传递数据。
- 父组件通过
-
兄弟组件通信:
- 通过父组件作为中介,父组件使用事件处理器更新数据并传递给其他兄弟组件。
-
跨层级组件通信:
- 通过 Event Bus(事件总线)来实现跨组件通信。
- 通过 Vuex 进行全局状态管理,适用于更复杂的组件通信需求。
选择合适的通信方式取决于你的应用结构、组件之间的关系以及数据流的复杂度。在大型应用中,通常会使用 Vuex 来管理全局状态,而在小型或中型应用中,props
和 $emit
可能已经足够满足需求。
3.5 props 校验
在 Vue 中,props
是父组件向子组件传递数据的主要方式。为了确保传递给子组件的数据符合预期的格式,Vue 提供了 props
校验 功能。通过在子组件中定义 props
的类型和规则,我们可以让 Vue 在开发环境下自动检查父组件传递的 props
是否符合指定的类型和约束条件。
props
校验的基础语法
在 Vue 中,我们可以通过在子组件的 props
选项中定义一个对象来指定属性的类型和其他验证规则。这些验证规则会在开发模式下进行检查并给出警告(如果数据不符合规则)。
props: {
// prop1 是一个字符串类型
prop1: {
type: String,
required: true, // 表示这个 prop 是必填的
default: 'default value' // 默认值
},
// prop2 是一个数字类型,且有自定义验证规则
prop2: {
type: Number,
validator(value) {
// 如果 prop2 的值小于 10,验证失败
if (value < 10) {
console.warn('prop2 should be greater than or equal to 10');
return false;
}
return true;
}
}
}
常见的 props
校验规则
-
type
type
用来指定prop
的类型。常见的类型有:String
、Number
、Boolean
、Array
、Object
、Function
、Symbol
等。Vue 会自动检查传递的数据是否符合指定的类型。props: { name: { type: String, required: true }, age: { type: Number, default: 18 } }
-
required
required
属性用来指定该prop
是否是必需的。如果required
设置为true
,Vue 会确保该prop
必须传递给子组件。props: { name: { type: String, required: true } }
-
default
default
用来指定该prop
的默认值。如果父组件没有传递该prop
,则会使用默认值。props: { name: { type: String, default: 'Anonymous' } }
注意:
default
只能用于非required
的props
,因为required
的props
必须由父组件提供值。 -
validator
validator
用来实现自定义的校验逻辑。它是一个函数,接受value
参数,返回true
或false
。如果返回false
,则表示验证失败,并在开发模式下发出警告。props: { age: { type: Number, validator(value) { // 验证 prop `age` 是否在合理的范围内 if (value < 0 || value > 120) { console.warn('Invalid age value'); return false; } return true; } } }
注意:
validator
只会在开发模式下有效,它不会在生产环境中执行。 -
自定义多个类型的
prop
校验如果一个
prop
可以是多个类型(例如,String
或Number
),可以使用数组来定义type
。props: { value: { type: [String, Number], required: true } }
value
可以是字符串类型或数字类型,Vue 会验证它是否符合这两者中的任意一种。
组合校验规则
可以在 props
中同时使用多个校验规则。
props: {
name: {
type: String,
required: true, // 必填
default: 'Guest' // 默认值
},
age: {
type: Number,
required: true,
validator(value) {
if (value < 0 || value > 120) {
console.warn('Age must be between 0 and 120');
return false;
}
return true;
}
}
}
校验规则的执行时机
- 校验只在 开发模式 下进行。在生产环境中,Vue 会移除这些校验逻辑,以提高性能。
- 如果
props
校验失败,Vue 会在开发者工具控制台输出警告信息,告诉你哪个prop
验证失败了,并给出相关的错误信息。 validator
函数只在开发环境中生效,当需要对生产环境进行严格的校验,可以考虑通过其他方式处理。
代码
<!-- ParentComponent.vue -->
<template>
<div>
<child-component :name="parentName" :age="parentAge"></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
parentName: 'John',
parentAge: 25
};
}
}
</script>
<!-- ChildComponent.vue -->
<template>
<div>
<p>Name: {{ name }}</p>
<p>Age: {{ age }}</p>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
required: true,
default: 'Anonymous'
},
age: {
type: Number,
required: true,
validator(value) {
if (value < 0 || value > 120) {
console.warn('Age should be between 0 and 120');
return false;
}
return true;
}
}
}
}
</script>
总结
在 Vue 中,props
校验可以帮助确保父组件传递给子组件的数据符合预期的类型和格式。
type
:指定类型;required
:指定是否为必填项;default
:指定默认值;validator
:自定义验证规则。
使用这些规则,Vue 会在开发模式下自动进行校验,确保传递的数据是有效的,并通过警告提醒开发者。在生产环境中,校验会被移除,以提高性能。
3.6 props
和 data
:单向数据流
在 Vue 中,props
和 data
是两种核心的数据机制,分别用于父子组件间的数据传递和组件内部的数据存储。理解这两者的关系及其如何支持 单向数据流 是理解 Vue 数据流动的关键。
1. props
和 data
的区别
(1) props
props
是父组件传递给子组件的数据。- 它是一种 单向数据流 的方式,数据只能从父组件流向子组件。子组件无法直接修改从父组件传递的
props
,但可以通过事件或其他方式与父组件进行交互。 props
的值是由父组件提供的,一旦传递给子组件,子组件可以通过props
来读取这些值。
<!-- ParentComponent.vue -->
<template>
<div>
<child-component :message="parentMessage"></child-component>
</div>
</template>
<script>
export default {
data() {
return {
parentMessage: 'Hello from Parent'
};
}
}
</script>
<!-- ChildComponent.vue -->
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
props: ['message']
}
</script>
parentMessage
通过 props
被传递给了子组件 ChildComponent
,子组件通过 {{ message }}
来访问并显示这个数据。这是一种单向数据流:从父组件到子组件。
(2) data
data
是组件内部的状态,它存储了组件的响应式数据。- 这些数据是 局部的,只能在组件内部使用或通过方法进行修改,不会直接影响其他组件的状态。
<!-- MyComponent.vue -->
<template>
<div>
<p>{{ message }}</p>
<button @click="changeMessage">Change Message</button>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello from data'
};
},
methods: {
changeMessage() {
this.message = 'Message changed!';
}
}
}
</script>
message
是一个在组件内部定义的 data
,它是响应式的,子组件可以通过 this.message
访问和修改它。点击按钮后,message
的值会被更新,Vue 会自动重新渲染视图。
2. 单向数据流的概念
单向数据流 是指数据只能在应用中以一个方向流动。在 Vue 中,数据流动的方向是从 父组件到子组件,通过 props
进行传递。Vue 的响应式系统保证了父组件的数据变更会自动反映到子组件中,而子组件无法直接修改父组件的数据。
这种设计理念的核心目的是:
- 可维护性:数据流动只有一个方向,避免了多个组件之间的数据相互依赖和复杂的更新逻辑,使得应用更易于调试和维护。
- 简化管理:当数据的流动是单向时,组件的状态和行为更容易追踪,父组件负责管理全局状态,子组件则负责展示和交互。
3. 如何遵循单向数据流
(1) 父组件向子组件传递数据 (Props)
父组件将数据通过 props
传递给子组件,子组件只能读取这些数据,不能修改它们。这种方式确保了数据流动的方向是单向的。
<!-- Parent.vue -->
<template>
<div>
<child-component :message="parentMessage"></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
data() {
return {
parentMessage: 'Message from Parent'
};
}
}
</script>
<!-- Child.vue -->
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
props: {
message: String
}
}
</script>
parentMessage
通过 props
传递给了 ChildComponent
,而子组件 ChildComponent
只是读取了这个值,不能修改它。
(2) 子组件通过事件通知父组件 (Event Emitting)
当子组件需要与父组件交互(例如修改父组件的数据)时,子组件不能直接修改父组件的数据,而是通过 自定义事件 通知父组件进行修改。父组件接收到事件后,执行相应的操作。
<!-- Parent.vue -->
<template>
<div>
<child-component @updateMessage="handleUpdateMessage"></child-component>
<p>{{ parentMessage }}</p>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
data() {
return {
parentMessage: 'Initial Message'
};
},
methods: {
handleUpdateMessage(newMessage) {
this.parentMessage = newMessage;
}
}
}
</script>
<!-- Child.vue -->
<template>
<div>
<button @click="sendMessageToParent">Change Parent Message</button>
</div>
</template>
<script>
export default {
methods: {
sendMessageToParent() {
this.$emit('updateMessage', 'Updated Message from Child');
}
}
}
</script>
子组件通过 this.$emit
向父组件发送 updateMessage
事件,父组件通过监听这个事件来更新自己的状态。这仍然是 单向数据流,因为父组件的状态变化是通过事件触发的,而不是直接由子组件修改的。
4. 为什么需要单向数据流?
(1) 数据追踪更简单
在单向数据流中,数据总是从父组件流向子组件,避免了多方向的数据传递问题,这样可以让开发者更容易追踪数据的变化来源。
(2) 调试和维护更容易
单向数据流使得 Vue 应用的行为更加可预测。当应用变得复杂时,我们可以通过父组件和子组件的关系清楚地理解数据是如何变化的,避免了状态不一致和不必要的复杂性。
(3) 减少了副作用
当子组件不能直接修改父组件的数据时,避免了多个组件之间不经意间互相影响的数据更改,减少了副作用。父组件可以集中管理数据,而子组件则专注于展示和交互。
(4) 便于构建组件库
由于 Vue 提倡单向数据流,开发者可以更容易地创建和维护可复用的组件。组件之间的关系是通过 props
和事件来建立的,父组件控制着数据流动,而子组件则是 纯粹的展示组件,不会修改外部的状态。
5. Vuex:管理复杂应用的单向数据流
在大型应用中,单一的父子组件数据流可能会变得不够用,尤其是当多个组件需要共享和修改相同的状态时。为了处理这种情况,Vue 提供了 Vuex,它是一个专门为 Vue 设计的状态管理库,可以帮助管理和共享跨组件的数据。
- Vuex 强制应用的数据流仍然是单向的。通过 state 来集中存储数据,mutations 用于修改数据,actions 用于触发 mutations,getters 用于计算派发的数据。
- Vuex 保证数据流动的单向性,使得应用的状态变得更加可预测和可维护。
总结
props
是用于父组件向子组件传递数据的机制,确保了数据流动是单向的。data
是组件内部的状态,只有组件内部可以修改它。- 单向数据流 是指数据从父组件流向子组件,而子组件不能直接修改父组件的数据。如果需要修改父组件的数据,子组件应该通过自定义事件向父组件发送请求,或者使用像 Vuex 这样的全局状态管理解决方案。
单向数据流在 Vue 中的应用有助于提高应用的可维护性、可预测性和调试性,使得组件的行为更加明确和稳定。
四、非父子组件通信
通过 provide
和 inject
进行跨层级通信
在 Vue 中,provide
和 inject
是一对 API,允许在组件树中实现跨层级的数据共享,通常用于避免通过 props
和 events
传递数据,尤其是当数据需要在多个组件之间共享时。
这两个 API 允许 祖先组件 提供数据,而 后代组件 无论在组件树中有多少层级,都可以直接访问这些数据。这样做的好处是能够避免多层级的 props
传递,减少了组件间的耦合性。
1. provide
和 inject
的基本原理
provide
:在祖先组件中提供一个数据或方法,这个数据是可以被后代组件访问的。inject
:在后代组件中接收祖先组件提供的数据。
这种机制主要是用来处理父组件和子组件之间不直接的通信,尤其适合跨越多层级的通信(例如跨越中间组件)。provide
和 inject
可以在不涉及 props
的情况下,使数据流在组件之间传递。
2. 使用场景
- 插件和全局设置:插件或库中的设置、主题配置等。
- 跨层级的数据共享:避免多层级的
props
传递,尤其在一些复杂的组件树中,传递props
会显得很冗余。 - 避免中间层组件的重复传递:一些中间层组件只是负责将
props
继续传递到更深的组件,并不使用它们,可以通过provide
和inject
来避免中间层的重复传递。
3. 基本用法
3.1 祖先组件使用 provide
提供数据
provide
让你可以提供一个对象、数据或方法,后代组件可以通过 inject
访问这些数据。
// GrandparentComponent.vue
<template>
<div>
<parent-component></parent-component>
</div>
</template>
<script>
import ParentComponent from './ParentComponent.vue';
export default {
components: { ParentComponent },
provide() {
// 提供数据
return {
message: 'Hello from Grandparent!',
updateMessage: this.updateMessage
};
},
methods: {
updateMessage(newMessage) {
this.message = newMessage;
}
}
};
</script>
GrandparentComponent
使用 provide
提供了 message
和 updateMessage
,这将允许其后代组件访问这些数据。
3.2 后代组件使用 inject
接收数据
inject
允许后代组件直接访问祖先组件通过 provide
提供的数据,无论组件层级有多深。
// ChildComponent.vue
<template>
<div>
<p>{{ message }}</p>
<button @click="changeMessage">Change Message</button>
</div>
</template>
<script>
export default {
inject: ['message', 'updateMessage'], // 注入祖先组件提供的数据和方法
methods: {
changeMessage() {
this.updateMessage('Message updated by Child!');
}
}
};
</script>
在 ChildComponent.vue
中,inject
会接收 GrandparentComponent
提供的 message
和 updateMessage
方法。message
显示在页面中,而 changeMessage
方法会调用 updateMessage
来更新 message
。
4. 工作原理
- 数据流动方向:数据流从祖先组件通过
provide
流向所有后代组件,这个过程是单向的。 - 响应式:提供的数据是响应式的,即当数据改变时,所有使用
inject
注入该数据的组件会自动重新渲染。注意,这种响应性是基于 Vue 的响应式系统的,数据本身需要是 Vue 的响应式对象。 - 作用域:
provide
和inject
是 跨层级的,但它们在组件树中仅限于祖先和后代组件之间的传递,不能跨越同级组件。如果两个组件没有父子关系,无法直接使用provide
和inject
。
5. 举例说明
5.1 跨层级数据共享
<!-- GrandparentComponent.vue -->
<template>
<div>
<parent-component></parent-component>
</div>
</template>
<script>
import ParentComponent from './ParentComponent.vue';
export default {
components: { ParentComponent },
provide() {
return {
userName: 'John Doe' // 提供的数据
};
}
};
</script>
<!-- ParentComponent.vue -->
<template>
<div>
<child-component></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent }
};
</script>
<!-- ChildComponent.vue -->
<template>
<div>
<p>{{ userName }}</p>
</div>
</template>
<script>
export default {
inject: ['userName'], // 注入祖先组件提供的数据
};
</script>
GrandparentComponent
提供了一个 userName
,而 ChildComponent
直接注入了这个数据。即使 ParentComponent
不直接使用 userName
,ChildComponent
依然能够访问到它。
5.2 跨层级函数共享
<!-- GrandparentComponent.vue -->
<template>
<div>
<parent-component></parent-component>
</div>
</template>
<script>
import ParentComponent from './ParentComponent.vue';
export default {
components: { ParentComponent },
provide() {
return {
logMessage: this.logMessage
};
},
methods: {
logMessage(message) {
console.log('Message from Grandparent:', message);
}
}
};
</script>
<!-- ParentComponent.vue -->
<template>
<div>
<child-component></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent }
};
</script>
<!-- ChildComponent.vue -->
<template>
<div>
<button @click="triggerLog">Log Message</button>
</div>
</template>
<script>
export default {
inject: ['logMessage'], // 注入祖先组件提供的方法
methods: {
triggerLog() {
this.logMessage('Hello from ChildComponent!');
}
}
};
</script>
GrandparentComponent
提供了一个方法 logMessage
,并且 ChildComponent
可以直接调用它。在点击按钮时,ChildComponent
调用了 logMessage
方法,输出信息到控制台。
6. provide
和 inject
的注意事项
- 数据的响应性:
provide
提供的数据是响应式的,但必须确保传递的数据是 Vue 可响应的对象。如果是非响应式对象,Vue 无法自动更新组件。 - 命名约定:
provide
和inject
的值需要遵循一致的命名约定,如果组件层级较多,使用命名空间可以帮助管理这些数据。 - 仅限于父子关系:
provide
和inject
是用于父子关系的跨层级通信,不能用于同级组件之间的通信。如果需要同级组件之间通信,可以考虑使用 Event Bus 或 Vuex。 - 生命周期:
inject
的数据在组件生命周期内是有效的,但当组件销毁时,数据也会被清除。需要特别注意清理事件监听和其他可能的副作用。
7. 总结
provide
和inject
机制非常适用于跨层级的数据共享,尤其是在多个中间组件没有必要传递数据时。- 通过
provide
和inject
,你可以减少不必要的props
传递,从而让你的组件更加简洁和解耦。 - 这种方式适用于一些特殊场景,比如全局设置、跨层级的共享数据,或者插件化的设计。
虽然 provide
和 inject
很有用,但对于应用状态比较复杂的情况,Vuex 仍然是更推荐的全局状态管理工具。