概述
在 Vue 中一个项目中往往需要被拆分成多个组件,但是每个组件之间都会有相互访问数据的需求。这时就涉及到组件之间的通讯了。
使用 props 进行组件间的通讯
父向子通讯
这里让 App 组件向 School 组件传入一组数据,使用 props 进行数据接收。
App.vue
<template>
<div id="app">
<h1>APP:</h1>
<School :school="school" :detail="detail"></School>
</div>
</template>
<script>
import School from './components/School.vue'
export default {
name: 'App',
components: {
School
},
data() {
return {
school: {
name: "首经贸",
address: "北京丰台"
}
}
},
}
</script>
School.vue
<template>
<div id="school">
<h2>学校: {{school.name}}</h2>
<h2>地址: {{school.address}}</h2>
</div>
</template>
<script>
export default {
props: ["school", "detail"]
}
</script>
注意:
- 传入的数据由于使用了 props 接收,所以 Vue 中不允许对数据进行修改,否则将会报错。
- 但是由于我们传入的是对象类型,并不是字符串类型,我们对对象中的数据进行修改,Vue 将无法检测报错,但是并不推荐这么做。
子向父通讯
字向父通过 props 进行通讯的原理和父向子类似,只不过我们这里利用了一个小技巧。
我们在父组件中定义一个函数,在函数中完成对自己数据的修改。然后我们通过 props 将这个函数传递给子组件。
子组件接收到函数后,可以对函数进行调用,这样就完成了子组件向父组件的通讯。
App.vue
<template>
<div id="app">
<h1>APP:</h1>
<School :school="school" :changeName="changeName"></School>
</div>
</template>
<script>
import School from './components/School.vue'
export default {
name: 'App',
components: {
School
},
data() {
return {
school: {
name: "首经贸",
address: "北京丰台"
}
}
},
methods: {
// 定义修改数据的函数
changeName(name) {
this.school.name = name;
}
}
}
</script>
使用组件自定义事件进行子向父通讯
我们可以在父组件使用子组件的时候,在子组件标签上在子组件的 vc 上绑定组件自定义事件。
指定子组件触发事件时,调用父组件的方法。通过方法来达成通讯内容。
而在子组件这里,可以使用 vc.$emit 手动触发。
使用组件标签绑定组件自定义事件
App.vue
<template>
<div id="app">
<h1>APP:</h1>
<Student @getName="demo"></Student>
</div>
</template>
<script>
import Student from './components/Student.vue'
export default {
name: 'App',
components: {
Student
},
methods: {
// 定义修改数据的函数
changeName(name) {
this.school.name = name;
}
},
}
</script>
Student.vue
<template>
<div id="student">
<h2>学生: {{name}}</h2>
<h2>年龄: {{age}}</h2>
<button @click="sendF">通讯</button>
</div>
</template>
<script>
export default {
data() {
return {
name: "brokyz",
age: 21
}
},
methods: {
sendF() {
this.$emit('getName', this.name)
}
}
}
</script>
使用 $on 绑定组件自定义事件
这种方法更加灵活。
App.vue
<template>
<div id="app">
<h1>APP:</h1>
<!-- <Student @getName="demo"></Student> -->
<Student ref="student"></Student>
</div>
</template>
<script>
import School from './components/School.vue'
import Student from './components/Student.vue'
export default {
name: 'App',
components: {
Student
}
methods: {
// es6 以数组的形式接收剩余参数
demo(name, ...params) {
console.log('App得到了子组件的name:', name);
}
},
mounted() {
this.$refs.student.$on('getName', this.demo);
// 指定只触发一次
// this.$refs.student.$once('getName', this.demo);
}
}
</script>
解绑组件自定义事件
当我们进行完通讯时,可以选择使用 vc 的 $off 解绑 vc 上面的自定义事件。
<template>
<div id="student">
<h2>学生: {{name}}</h2>
<h2>年龄: {{age}}</h2>
<button @click="sendF">通讯</button>
<button @click="unbind">解绑事件</button>
</div>
</template>
<script>
export default {
data() {
return {
name: "brokyz",
age: 21
}
},
methods: {
sendF() {
this.$emit('getName', this.name)
},
unbind() {
// 解绑多个事件以数组的形式传入多个需要解绑的事件
this.$off('getName')
console.log("事件已解绑")
}
}
}
</script>
关于组件自定义事件的注意点
- 如果使用 ref 和 \(on 的方法进行自定义事件的时候,\)on 里面的回调函数需要是箭头函数和钩子。如果使用了正常的回调函数声明,那么里面的 this 指向的是子组件 vc,并不是当前组件。因为这个是由 this.$refs.xxx 调用的。
- 如果在组件标签上添加 dom 原生的事件,那么组件都将其看成是自定义事件。需要添加修饰才能将其识别为原生 dom 事件。
@click.native
- 给谁绑定事件,就需要谁触发事件。
全局事件总线
全局事件总线可以完成任意间组件通讯。
我们通过一个组件外并且所有组件都可以访问到的拥有事件绑定 $on 和事件触发功能 $emit 的对象,通过在这个对象上进行事件的绑定与触发来达到组件间通讯的目的。我们管这个操作叫做全局事件总线。
由于我们需要的对象,要有 $on 和 $emit 方法,所以可供我们选择的对象就只有 vm 和 vc 了。事实上这二者都可以完成。但是我们使用 vm 作为全局事件总线,将会更加方便。
我们确定了全局事件总线由 vm 担任后,我们需要让所有的组件都可以访问到全局事件总线才行。这时由于 vm 和 vc 之间的原型关系我们知道,在 vm 原型上绑定的属性可供全体组件读取。因此我们需要在 vm 的原型身上绑定一个全局事件总线,这个全局事件总线由 vm 担任,我们将其赋值为 $bus。
下面是 test1 组件向 test2 组件通讯的实例
main.js
import Vue from "vue"
import App from "./App.vue"
Vue.config.productionTip = false
new Vue({
render: (h) => h(App),
beforeCreate() {
Vue.prototype.$bus = this
}
}).$mount("#app")
test1.vue
<template>
<div>
<button @click="sendF">点击触发事件</button>
</div>
</template>
<script>
export default {
methods: {
sendF() {
// 触发事件 demo,向回调函数传值 666
this.$bus.$emit("demo", 666)
}
}
}
</script>
test2.vue
<script>
export default {
mounted() {
// 绑定事件 demo 触发时执行回调函数
this.$bus.$on("demo", (msg) => {
console.log(msg)
})
},
beforeDestroy() {
// 销毁前解绑事件 demo
this.$bus.$off("demo")
}
}
</script>
注意:
- 相对于自定义事件,全局事件总线需要在组件销毁前解绑。因为自定义事件中是绑定在当前组件的,组件销毁时自定义事件自然销毁。而全局事件总线式绑定在 vm 的原型对象上的,如果组件销毁,事件依然存在。因此需要我们手动去解绑。
消息订阅与发布
消息订阅与发布需要借助第三方库,我们在这里推荐 pubSub-js。
npm i pubsub-js
我们使用时可以只在需要的组件中导入,也可以直接导入到 vm.prototype 中全局使用。
下面同时使用了全局事件总线和消息订阅与发布,可以对比二者。
main.js
import Vue from "vue"
import App from "./App.vue"
import pubsub from "pubsub-js"
// 将 pubsub 添加到全局
Vue.prototype.ps = pubsub
Vue.config.productionTip = false
new Vue({
render: (h) => h(App),
beforeCreate() {
// 为全局添加全局事件总线
Vue.prototype.$bus = this
}
}).$mount("#app")
test1.vue (发布者)
<template>
<div>
<button @click="sendF">点击触发事件</button>
</div>
</template>
<script>
export default {
methods: {
sendF() {
// 全局事件总线触发 demo 事件,并向回调函数传入 666
this.$bus.$emit("demo", 666)
// 发布消息,并向订阅了 hello 的人传递 777
this.ps.publish("hello", 777)
}
}
}
</script>
test2.vue (订阅者)
<script>
export default {
mounted() {
// 绑定事件 demo 与 事件被触发时的回调函数
this.$bus.$on("demo", (msg) => {
console.log(msg)
})
// 订阅消息 hello,并指定接收到发布消息的回调函数
this.psId = this.ps.subscribe("hello", (msgName, msg) => {
console.log(msgName, msg)
})
},
beforeDestroy() {
// 解绑消息
this.$bus.$off("demo")
// 取消订阅
this.ps.unsubscribe(this.psId)
}
}
</script>
注意:
- 订阅消息时,传入回调函数的形参有两个,第一个是消息名,第二个才是传递过来的消息。如果不需要第一个参数,可以使用下划线占位。
(_, msg)
- 订阅消息时,会返回一个唯一的参数,需要接收这个参数。以便于在取消订阅的时候使用。
- 对比来看,消息订阅与发布和全局事件总线十分相似。
Vuex
一个用于管理 Vue 中所有组件数据的插件,可以很轻松的实现组件间的通讯。
安装 Vuex:
如果在 Vue2 上需要安装 vuex3,因为最新的 vuex4 只支持 vue3.
npm i vuex@3
npm i vuex
vuex 配置
我们需要在 src 目录下,新建 store 文件夹,在文件夹中新建 index.js。
index.js 中就是 vuex 的相关配置。
demo:index.js
// 该文件用于创建Vuex中最为核心的store
import Vue from "vue"
// 引入Vuex
import Vuex from "vuex"
// 准备actions——用于响应组件中的动作
const actions = {
}
// 准备mutations——用于操作数据(state)
const mutations = {
}
// 准备state——用于存储数据
const state = {
}
// 令 Vue 实例使用插件 Vuex,注意,此步骤必须在创建 store 实例之前。
Vue.use(Vuex)
// 创建并暴露 store
export default new Vuex.Store({
actions,
mutations,
state
})
注意:
我们在使用插件时,在 index.js 中,这样才可以保证使用 Vuex 插件的顺序在创建 store 实例之前。
如果我们在 main.js 中引用时,所有的 import 都会存在变量提升,所以不可能使得 use(Vuex) 在创建 store 实例之前。
vuex 的使用
原理:
经过以上配置之后,vc 上将会存在 \(store 属性,我们可以通过 `this.\)store`来使用 vuex。
在组件上,我们可以通过this.$store.dispatch(name, value)
来将调用 actions 中的函数,并向内传递 value。
在 actions 中,函数接收两个参数,如jia(context, value){}
其中 context 为上下文,是一个对象,里面有 dispatch、commit 方法;value 是接收到的值。一般来说,此方法多用于,对传入的数据进行一些处理或者逻辑判断。调用 context 中的 commit 方法将数据发送给 mutations,如context.commit(name, value)
在 mutations 中,函数也会接收两个参数,jia(state, value){}
,其中 state 为存储的数据的数据代理对象。可以从过它,对数据进行操作。数据变更时,vue 也会进行渲染。
在 getters 中,可以让我们返回经过我们加工的数据。有点类似于计算属性,但是计算属性只能在当前组件使用,getters 可以配置在 vuex 中,所有组件调用时都是 getters 中加工后的数据。
当我们使用到 vuex 中的数据时,可以使用this.$store.state.xxx
来读取数据。
至此 vuex 的通信流程结束。
demo:
index.js
// 该文件用于创建Vuex中最为核心的store
import Vue from "vue"
// 引入Vuex
import Vuex from "vuex"
// 准备actions——用于响应组件中的动作
const actions = {
jia(context, value) {
console.log(context)
context.commit("jia", value)
},
jian(c, value) {
c.commit("jian", value)
}
}
// 准备mutations——用于操作数据(state)
const mutations = {
jia(state, value) {
state.sum += value
},
jian(s, value) {
console.log(s)
s.sum -= value
}
}
// getters 返回数据加工
const getters = {
bigSum(state) {
return state.sum * 10
}
}
// 准备state——用于存储数据
const state = {
sum: 0
}
Vue.use(Vuex)
// 创建并暴露store
export default new Vuex.Store({
actions,
mutations,
state,
getters
})
Count.vue
<template>
<div>
<h2>当前求和为: {{ $store.state.sum }}</h2>
<h2>当前求和×10: {{ $store.getters.bigSum }}</h2>
<select v-model.number="n">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="increment">+</button>
<button @click="decrease">-</button>
<button @click="oddPlus">当前求和为奇数再加</button>
<button @click="waitJia">等一等再加</button>
</div>
</template>
<script>
export default {
name: "Count",
data() {
return {
n: 1
}
},
methods: {
increment() {
this.$store.dispatch("jia", this.n)
},
decrease() {
this.$store.commit("jian", this.n)
},
oddPlus() {
// 这些逻辑判断应该写在 actions 中
if (this.$store.state.sum % 2 != 0) {
this.increment()
console.log(this.n % 2);
}
},
waitJia() {
setTimeout(() => {
this.increment()
}, 1000)
}
}
}
</script>
vuex 代码优化
mapState & mapGetters
在使用 vuex 时,我们如果要访问 vuex 中的数据,我们会使用标准写法this.$store.state.xxx
。
但是如果我们调用的数据过多,就需要书写很多一大长串的前缀去调用。
vuex 考虑到了这种不便,为我们 mapSate 和 mapGetters 方法,让我们去优化代码。
<template>
<div>
<h2>{{ bigSum }}</h2>
<p>{{ sum }},{{ test }}</p>
</div>
</template>
<script>
// 引入 mapState 和 mapGetters
import { mapState, mapGetters } from "vuex"
export default {
name: "Count",
computed: {
// ...mapState({sum1:"sum",sum2:"sum"})
// 同名简写
...mapState(["sum", "test"]),
...mapGetters(["bigSum"])
}
}
</script>
mapActions & mapMutations
帮助我们简写方法
<template>
<div>
<h2>当前求和为: {{ sum }}</h2>
<h2>当前求和×10: {{ bigSum }}</h2>
<select v-model.number="n">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="jia(n)">+</button>
<button @click="jian(n)">-</button>
<button @click="oddPlus">当前求和为奇数再加</button>
<button @click="waitJia">等一等再加</button>
<p>{{ sum }},{{ test }}</p>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions, mapMutations } from "vuex"
export default {
name: "Count",
data() {
return {
n: 1
}
},
computed: {
// ...mapState({sum1:"sum",sum2:"sum"})
// 同名简写
...mapState(["sum", "test"]),
...mapGetters(["bigSum"])
},
methods: {
// ...mapActions({increment:"jia"}),
// ...mapMutations({decrease:"jian"}),
// 同名简写
...mapActions(["jia"]),
...mapMutations(["jian"]),
// increment() {
// this.$store.dispatch("jia", this.n)
// // console.log(this);
// },
// decrease() {
// this.$store.commit("jian", this.n)
// },
oddPlus() {
if (this.$store.state.sum % 2 != 0) {
this.jia(this.n)
console.log(this.n % 2)
}
},
waitJia() {
setTimeout(() => {
this.jia(this.n)
}, 1000)
}
}
}
</script>
vuex 模块化
在 vuex 中不同的功能之间可以分类,将他们模块化。
index.js
// 该文件用于创建Vuex中最为核心的store
import Vue from "vue"
// 引入Vuex
import Vuex from "vuex"
// 求和相关的配置
const countOptions = {
namespaced: true,
actions: {},
mutations: {},
getters: {},
state: {}
}
// 人员相关的配置
const personOptions = {
namespaced: true,
actions: {},
mutations: {},
getters: {},
state: {}
}
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
countAbout: countOptions,
personAbout: personOptions
}
})
Count.vue
<template>
<div>
<h2>{{ bigSum }}</h2>
<p>{{ sum }},{{ test }}</p>
</div>
</template>
<script>
// 引入 mapState 和 mapGetters
import { mapState, mapGetters } from "vuex"
export default {
name: "Count",
computed: {
// 使用时通过 countAbout.xxx
// ...mapState(["countAbout","personAbout"]),
// 需要开启命名空间, 这样可以直接使用 xxx 不用通过 countAbout.xxx
...mapState("countAbout", ["xxx","xxx"]),
},
methods: {
add(){
// 使用模块化时,需要指定模块用/分割
this.$store.commit('personAbout/addPerson', 666)
}
}
}
</script>
数据包装
我们要进行传值时,有时需要传递的是对象,那么就需要将我们的数据包装成对象的形式传入。
对于唯一 id 的处理,我们可以使用 nanoid 包
npm i nanoid
数据包装:
import {nanoid} from 'nanodid'
const name = "brokyz"
const age = 21
const obj = {id:nanoid(), name:name, age:age}
标签:通讯,name,state,vue,事件,Vue2,组件,store
From: https://www.cnblogs.com/brokyz/p/16750433.html