通过阅读了官方文档和大量网友的博文,融入自己的想法,整理出对组件通信的看法,第一次发文,如有不周敬请谅解哦~
进行组件间通信的原因
组件实例的作用域是孤立的,但组件间的联系和交互不可避免。当组件间进行数据交互时,组件通信必不可少。
组件通信的类型
于是组件的通信可以分为两种情况:
父子组件间通信(父传子,子传父),非父子组件通信。
父传子类型
prop传值
step1:父组件用prop自定义属性传递属性,prop有字面量语法和动态语法。字面量只能将数据按字符串的形式传递;动态语法类似于v-bind,可以将父组件数据的实时变化传递给子组件。
step2:子组件需要用props来显式地声明prop。子组件props的写法有两种:
- 数组形式:
- 对象形式:可以进行条件约束
子传父
1.回调传参
父将函数用prop传递给子,子在调用这个函数时,将数据作为参数传递给父组件
父组件中:<child2 :changeParent="changeMyself"></child2>子组件中:methods: { // 父组件改变自身数据的方法 changeMyself (chidData) { this.parentChangeData = chidData } }export default { // 子组件显示声明props父组件的函数 props: { changeParent: { requried: true, // 代表是否为必传 type: Function // 代表数据类型 } }, data () { return { childData: '我要把数据传给父' } } }<button @click="changeParent(childData)">点我传递数据给父组件</button>
2.自定义事件+事件监听
子$emit自定义一个事件,第一个参数为事件名,第二个参数为数据。父v-on绑定一个事件监听器,在绑定的method中以参数形式获得子组件的数据。
子组件中:
methods: {
// 自定义事件$emit暴露数据,数据作为参数
changeParent () {
this.$emit('change', this.childData)
}
}父组件中:
<!-- 父组件绑定监听,得到子组件数据-->
<child3 @change="changeMyself"></child3>methods: {
// 父组件改变自身数据的方法
changeMyself (chidData) {
this.parentChangeData = chidData
}
}3.直接访问
用ref对子组件进行标记,父组件直接通过this.$refs[子组件ref].[子组件属性/方法]来获得子组件的数据。
注意:"$refs 只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案——应当避免在模版或计算属性中使用 $refs 。" 官网这么说的,只是应急用,一般不推荐这样使用
思考:单项数据流
Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。
另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。
在两种情况下,我们很容易忍不住想去修改 prop 中数据:
Prop 作为初始值传入后,子组件想把它当作局部数据来用;
Prop 作为原始数据传入,由子组件处理成其它数据输出。
对这两种情况,正确的应对方式是:
定义一个局部变量,并用 prop 的值初始化它:
定义一个计算属性,处理 prop 的值并返回:
思考:子与父的双向绑定
单项数据流导致子组件不能直接去改动父组件的数据。但真实场景中,有很多时候需要在子组件中更改父组件的数据。
方法:
父有一个改变其自身数据的方法,用prop传给子,子在需要改变父组件数据的时候调用该方法。(与回调参数方法一致)
子组件通过事件发送,$emit事件将数据传给父组件,父组件监听后自己执行改变自身数据的方法。(与子传父的第二种方法一致)
自己想到的方法:父组件有一个改变自身数据的方法,子通过this.$parent.[父组件方法]直接调用父组件方法,将子组件的数据作为参数传递给父组件从而改变父组件数据
父组件中:
methods: { // 父组件改变自身数据的方法 changeMyself (chidData) { this.parentChangeData = chidData } }子组件中:
export default { data () { return { childData: '我是子要传给父的数据' } }, methods: { change () { // 通过直接访问的方法调用 this.$parent.changeMyself(this.childData) } } }
但以上的方法都是基于事件触发的,不能保证子父组件的数据时刻同步
4.使用.sync绑定修饰符在父组件prop中显示强调双向绑定。子组件在watch中使用$emit(‘update:data’,newVal)监听,更新父组件prop传过来的数据,父组件不需要再监听该update方法。
父组件中:
<!-- 显式绑定.sync修饰符 --> <child6 :parentData.sync= "parentData"></child6>子组件中:
props: ['parentData']// 子组件进行数据监听 watch: { childData (newVal, oldVal) { this.$emit('update:parentData', newVal) } }
这里的自定义事件与子传父的自定义事件有一些不同。
<child6 :parentData.sync="parentData"></child6>
会被扩展为:
<child6 :parentData"parentData" @update:parentData="val => parentData = val"></child6>
思考:
自定义事件发生时候运行的响应表达式是<child6 :parentData="parentData" @update:parentData="val => parentData = val"></child6>中的 "val => bar = val"。
在子传父的“通过$emit事件从子组件向父组件中传递数据” 里,自定义事件发生时候运行的响应表达式是:<child @chang="changeMyself"></child>中的changeMyself。
对前者, 表达式 val => bar = val意味着强制让父组件的数据等于子组件传递过来的数据, 这个时候,我们发现父子组件的地位是平等的。 父可以改变子(数据), 子也可以改变父(数据)。
对后者, changeMyself是在父组件中定义的, 在这个函数里, 可以对从子组件接受来的数据做任意的操作或处理, 决定权完全落在父组件中, 也就是: 父可以改变子(数据), 但子不能直接改变父(数据)!, 父中数据的变动只能由它自己决定。
有一个投机取巧的办法:在父用prop传数据给子时,若子组件中props的类型为对象或数组时,可以直接在子组件中修改这个prop来的数据,且不会被vue检测报错,但这样会使得数据流变得更加难以分析,同时,当props的类型为引用数据类型时,要注意在子组件中对对象进行深拷贝,防止隐性修改父组件的对象。
兄弟传兄弟(非父子)
1.两个兄弟组件有共同的父组件
父组件将数据prop给一个兄弟A,将改变数据的方法prop给另一个兄弟B,B调用方法改变A的数据,将数据和改变数据提升到了父组件内部,然后再分发下去。父组件中:
<!-- 父组件把改变数据的方法传给child2,把数据传给child,将数据通信提升到父组件层次 -->
<child2 :changeParent="changeMyself"></child2>
<child :parent="parentChangeData"></child>child2中:
<!-- 子组件调用方法,将数据作为参数 -->
<button @click="changeParent(childData)">点我传递数据给父组件</button>export default {
// 子组件显示声明props父组件的函数
props: {
changeParent: {
requried: true, // 代表是否为必传
type: Function // 代表数据类型
}
},
data () {
return {
childData: '我要把数据传给父'
}
}
}child中:
// 显示声明props父组件的数据
props: {
parent: {
requried: true, // 代表是否为必传
type: String, // 代表数据类型
default: '我是默认值'
}
}2.路由传参
把需要跨页面传递的数据放到url后面,跳转到另外页面时直接获取url字符串获取想要的参数即可。
{
path: '/params/:id'
name: '',
component: Sample
}<router-link :to="params/12">跳转路由</router-link>在跳转后的组件中用$route.params.id去获取到这个id参数为12,但这种只适合传递比较小的数据,数字之类的。
3.EventBus
首先创建一个中央时间总线,在需要使用的组件里引入。组件A用this.Bus.$emit('eventName', value)触发事件,组件B用this.Bus.$on('eventName', value => { this.print(value) })接收事件。在$emit之前,必须已经$on, 因此普遍采用在created钩子中进行$on.
// 新建Bus.js文件
import Vue from 'vue'
export default new Vue()组件A传送数据:
<script>
// 在需要使用的组件中引用
import Bus from '@/components/Bus'
export default {
data () {
return {
childData: '我是兄弟A,把我的数据放进eventBus'
}
},
methods: {
submit () {
// 触发事件
Bus.$emit('change', this.childData)
}
}
}
</script>组件B接收数据:
get () {
// 监听接收事件
Bus.$on('change', value => {
this.myData = value
})
}// 组件销毁时,解除绑定
destroyed () {
Bus.$off('change')
}组件间的通信都可以用EventBus实现, 无论有多少个组件,只要保证eventName不一样就可以了,专门设置一个空的Bus实例来作为中央事件总线,而不是直接访问root,更清晰也更利于管理。
特殊的eventBus
传统的eventBus只负责$emit和$on,与数据没有任何的交集。这使得数据不是“长效”的,只在$emit后生效,并且同一组件多次生成会多次$on,路由切换时,还要考虑新组件的绑定和旧组件的解除绑定。
方法:考虑将$on放在Bus中完成,修改Bus为:
// 新建Bus.js文件
// Bus进行监听,将数据直接放在Bus中
import Vue from 'vue'
const Bus = new Vue({
data () {
return {
child7Val: ''
}
},
created () {
this.$on('change', value => {
this.child7Val = value
})
}
})
export default Bus发出数据的组件不变
<script>
// 在需要使用的组件中引用
import Bus from '@/components/Bus'
export default {
data () {
return {
childData: '我是兄弟A,把我的数据放进eventBus'
}
},
methods: {
submit () {
// 触发事件
Bus.$emit('change', this.childData)
}
}
}
</script>接收数据的组件修改为用计算属性直接从Bus中存取数据,使用计算属性是为了保证数据的动态性。
computed: {
child7Val () {
return Bus.child7Val
}
}Vuex
我的理解是,Vuex相当于一个大型的专门用来储存共享变量的仓库。
在安装Vuex之后,创建store.js文件
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
// 存放共享变量
msg: '我是原始数据'
},
getter: {
// 相当于store中的计算属性
mymsg: state => {
return state.msg
}
},
mutations: {
// 修改state,vue推荐使用大写
MUTATIONSMSG (state, payload) {
state.msg = payload.msg
}
},
actions: {
// 与mutations类似,支持异步
mutationsMsg (context, payload) {
context.commit('MUTATIONSMSG', payload)
}
},
modules: {
app
},
strict: process.env.NODE_ENV !== 'production'
})
export default store当使用mutations来修改state时:
组件A使用store中的数据:
<template>
<div class="child">
<h3>组件A</h3>
{{$store.state.msg}}
</div>
</template>组件B修改store中的数据:
<script>
export default {
data () {
return {
myData: '组件B的数据'
}
},
methods: {
get () {
this.$store.commit('MUTATIONSMSG', this.myData)
}
}
}
</script>当使用actions修改state时:
<script>
export default {
data () {
return {
myData: '组件B的数据'
}
},
methods: {
get () {
this.$store.dispatch('mutationsMsg', this.myData)
}
}
}
</script>state用来存放共享变量,通过this.$store.state.[变量]来获取共享变量的值。
getter,可以增加一个getter派生状态,(相当于store中的计算属性),store.getters.方法名()用来获得共享变量的值。
mutations用来存放修改state的方法(相当于set)。
actions也是用来存放修改state的方法,不过action是在mutations的基础上进行。在actions先commit mutations里的方法,再由使用actions的组件进行dispatch
思考
组件化的思想就是希望组件的独立性,数据能互不干扰
通过组件A直接去修改组件B的值,比如双向绑定,虽然方便,但增加了组件间的耦合性。最好就是如果组件A要修改组件B的值,那么就将修改的数据暴露出去,由B得到数据后,自行修改。