Loading

Vue2 组件间通讯

概述

在 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}
posted @ 2022-10-03 13:49  brokyz  阅读(75)  评论(0编辑  收藏  举报