Vue:数据与事件通信
Vue:数据与事件通信
Prop:父子组件间通信
类比于程序和函数,父组件是一个更大的抽象级别,往往作为数据的拥有者,而子组件则是作为数据处理的执行者。在父子组件间的数据传递方面,Vue 提供了一个属性:Prop。
我们可以在组件实例中访问 prop,就像我们访问 data 那样。
通过 Prop,父组件得以将数据交由子组件处理或详细表示。
<!-- 父组件 -->
<template>
<div id="main-view">
<task v-for="task in tasks" :title="task.title"></task>
</div>
</template>
<script>
import Task from '../components/Task.vue'
export default {
name: 'main-view',
data() {
return {
tasks: [
{title: 'list-1'},
{title: 'list-2'},
{title: 'list-3'}
]
}
},
components: {
Task
}
}
</script>
<!-- 子组件 -->
<template>
<div class='task'>
<span>{{ title }}</span>
</div>
</template>
<script>
export default {
name: 'task',
props: ['title']
}
</script>
父子组件通过 prop 进行数据传递,但是这一个数据传递是单向下行绑定的。在 Vue 中,父组件的值发生了变化,那么子组件中对应的 prop 数据是能够响应变化的,但是反过来不行。我们并不希望在子组件中能够直接更改父组件中的数据信息——子组件不应该有这么大的权限。对于 prop 这一单向数据流,从某个方面来说,类似于函数中的参数值传递。和函数值传递所不同的是,当我们在子组件中尝试对 prop 进行修改的时候,Vue 会在控制台抛出警告。

但是对于数组和对象这一类引用类型,我们不改变其与 prop 的绑定,而是改变其“内部值”。例如,我们可以向 prop 传入一个对象,在子组件中对对象的属性值进行更改。这并不会引起 Vue 对你的警告,但是在子组件中改变父组件中的数据并不是一个好的做法。
在很多时候,我们确实需要在子组件响应某种事件时(比如点击子组件),对数据进行更改。当然,我们并不希望子组件拥有如此大的权限对父组件的数据进行修改。父组件的数据修改应由父组件完成,或者由“更高级者”来完成。
为了实现父子组件间的交互,Vue 提供了 v-on 和 $emit 方法。
<!-- 子组件 -->
<div class='child-component'>
<button v-on:click="$emit('clickFunc')">
click me!
</button>
</div>
<!-- 父组件 -->
<father-component v-on:clickFunc="...">
<child-component></child-component>
</father-component>
父组件监听某一事件,如上方的 clickFunc ,当子组件抛出这一事件时,父级组件就会接收该事件并执行相关操作。
当然,对于组件要抛出的事件,你可以通过 emits 来显式声明:
export default {
emits: ['clickFunc', 'submit']
}
需要注意的是,具有直接关系父子组件才能够实现“抛出”和“监听”。对于平级(兄弟)组件以及更深层次的嵌套组件,无法直接通过 $emit 和 v-on 实现事件通信。我们可以通过全局事件总线来实现更为复杂的事件通信。
Provide 和 Inject:Prop 逐级透传问题的解决方案
实际应用中,页面结构不会仅仅有父子组件,深层次嵌套的组件组合是常见的:
<root>
<first-level>
<second-level>
<third-level></third-level>
</second-level>
</first-level>
</root>
假设这么一个场景:数据 message 由 root 所拥有,并被 third-level 所使用。数据 message 为局部数据,原则上平级组件不应该能够访问到该数据。Vue 中数据的传递通常由 prop 来完成,此处也可用 prop 实现数据的传递:root 将 message 传递给 first-level ,继而 first-level 传递给 second-level ,最终 message 由 second-level 交给 third-level 。虽然数据的传递过程略微冗长,但至少我们的目标实现了。不过除了传递过程冗长的问题外,似乎 first-level 和 second-level 不必要,也不应该接触到 message。
Vue 给出的解决方式是:使用 Provide 和 Inject 。“一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖”。
<!-- 依赖提供者 -->
export default {
provide: {
message: 'Hello World!'
}
}
<!-- 接收者 -->
export default {
inject: ['message']
}
对于 provide 对象上的每一个属性,后代组件会用其 key 为注入名查找期望注入的值,属性的值就是要提供的数据。
如果接收者溯源找不到期望值,那么将会抛出错误。在解决这一个问题前,我们先了解关于注入时的别名。当我们使用数组形式的 inject,注入的属性将会以同名的 key 暴露到组件实例上。也就是我们可以在接收者中使用 this.message 访问到 inject 中的 message 。如果我们想要一个新的名字,我们可以这么做:
export default {
inject: {
localMessage: {
from: 'message'
}
}
}
回到找不到期望值的问题。事实上,就如 prop 一般,我们可以为 inject 设置默认值:
export default {
inject: {
message: {
from: 'message', //当与注入名同名时,这个选项是可选的
default: 'default value'
}
}
}
当然,我们还可以提供当前组件实例的状态,如 data()中的数据。但需要进行修改,将 provide 以函数形式呈现:
export default {
data() {
return {
message: 'Hello World!'
}
},
provide() {
return {
message: this.message
}
}
}
但是这一方式并没有保持响应式。这就意味着当 data() 中的 message 由 'Hello World!' 变为 'Bye' 时,provide 中的 message 仍然是 'Hello World'。我们可以使用计算属性解决这一问题:
import { computed } from 'vue'
export default {
data() {
return {
message: 'Hello World!'
}
},
provide() {
return {
message: computed(() => this.message)
}
}
}
Bus:全局事件总线
关于 Bus ,其中文释义为:总线;公共汽车。“公共汽车”这一释义在此处也十分形象:对于所有人都是可见的,所有人都可以乘坐。
Bus 游走于各个组件,实现数据和事件的传递和分发。
首先我们需要绑定一个 Bus 实例:
<!-- main.js -->
Vue.prototype.$bus = new Vue()
这样,我们就可以在组件中使用 $bus 。Bus 打破了组件间事件通信的壁障,无论是平级组件还是深层次的嵌套组件都可以实现通信。当然,父子组件也可以使用 Bus 进行通信,不过父子组件可以直接使用 $emit 和 v-on 进行事件通信。
在组件创建或挂载时即对事件进行监听:
created()/mounted() {
this.$bus.$on('isCalled', this.isCalled)
}
而对于事件的抛出,只需要在事件发生处抛出即可:
<button @click="this.$bus.emit('isCalled')">
click me!
</button>
对于所注册的总线事件,我们需要在组件销毁前对事件解除监听,否则会出现多次挂载,导致一次触发但多个响应:
beforeDestroyed() {
this.$bus.$off('isCalled', this.Called)
}
需要注意的是,由于我们是在组件中监听事件,当一个组件被复用多次时,抛出事件后这些组件实例都会对事件进行响应——如果你正需要这一效果,那么可以充分利用这一性质。如果你只希望其中的某一个组件实例对此做出响应,那么就需要做出判断,或者以别的方式实现事件通信。
Store:仓库式管理状态
Vue 为项目状态管理提供了 Vuex ,但随着 Vue 的不断发展,现如今 Vue 更推荐使用 Pinia 对项目进行状态管理。
对 Vuex 而言,重要的是两个点:store 和 mutation 。前者为状态,后者为改变状态的手段。当涉及到异步操作时,需要用到 action 。
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default {
state: {
count: 0
}, // 在 Vuex 官网,采用的是函数返回对象式的写法
mutations: {
increment(state) {
state.count++
}
}
}
Vuex 不希望你在组件中直接改变 state ,即意味着在组件中 state 是只读的。要改变 state ,应该显式提交 mutation 。
export default {
data() {
},
computed: {
count() {
return this.$store.state.count
}
}
methods: {
test() {
this.$store.commit('increment')
}
}
}
Vuex 使用单一状态树对项目的状态进行管理——意味着仅需要一个对象就可以对整个项目的各个状态进行访问。同时,为了不使状态管理臃肿和混乱,我们可以将状态划分到不同的子模块中。
// moduleA.js
export default {
state: {
a: 'a'
},
mutations: {
...
}
}
// moduleB.js
export default {
state: {
b: 'b'
},
mutations: {
...
}
}
// index.js
import Vue from 'vue'
import Vuex from 'vuex'
import moduleA from './moduleA.js'
import moudleB from './moduleB.js'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
moduleA,
moduleB
}
})
在访问时:
// 组件文件中
export default {
data() {
...
},
computed: {
one() {
return this.$store.state.moduleA.a
}
}
}
参考: Vuex (vuejs.org)

浙公网安备 33010602011771号