vue组件通信方式
一、props
(父向子传值----自定义属性) / $emit(子向父传值----- 自定义事件)
父组件通过props
的方式向子组件传递数据,而通过$emit
子组件可以向父组件通信。
1. 父组件向子组件传值(props)
下面通过一个例子说明父组件如何向子组件传递数据:在子组件article.vue
中如何获取父组件section.vue
中的数据articles:['红楼梦', '西游记','三国演义']
// section父组件 | |
<template> | |
<div class="section"> | |
<com-article :articles="articleList"></com-article> | |
</div> | |
</template> | |
<script> | |
import comArticle from './test/article.vue' | |
export default { name: 'HelloWorld', components: { comArticle }, data() { return { articleList: ['红楼梦', '西游记', '三国演义'] } }} | |
</script> | |
// 子组件 article.vue | |
<template> | |
<div> | |
<span v-for="(item, index) in articles" :key="index">{{item}}</span> | |
</div> | |
</template> | |
<script> | |
export default { props: ['articles']} | |
</script> |
注意: prop 可以从上一级组件传递到下一级组件(父子组件),即所谓的单向数据流。而且 prop 只读,不可被修改,强行修改能生效,但是控制台会有错误信息。
在子组件修改父组件传入的值的方法:
1 .sync 父组件v-on绑定自定义属性时添加修饰符.sync 在子组件中通过调用emit(′update:自定义属性′,要修改的新值)==>emit('update:自定义属性',新值) 固定写法 此时子组件中接收的值就更新成了新值(父组件中的原始值会跟着变化,控制台不会报错)
父组件中: <child :value.sync='xxx'/>
子组件中: this.$emit('update:value',yyy)
2.在子组件data中声明自己的数据,让接收的数据作为这个数据的值 ==> 子组件的数据=this.value
(这种方法实际修改的是自己的数据 父组件的数据没变)
子组件的data中: 1.接收传入的数据: props:'value'
2.newValue=this.value
3.父组件传值时传递一个引用类型,在子组件中修改引用类型的属性值并不会改变该引用类型在堆中的地址
(这种方法会让子组件和父组件的引用类型属性的值同时更改)
子组件中: props:'value'
this.value'属性名' = 新值 或者使用 this.$set()方法也可以
2. 子组件向父组件传值($emit,props)
$emit
绑定一个自定义事件, 当这个语句被执行时, 就会将参数arg传递给父组件,父组件通过v-on监听并接收参数。 通过一个例子,说明子组件如何向父组件传递数据。 在上个例子的基础上, 点击页面渲染出来的ariticle
的item
, 父组件中显示在数组中的下标
// 父组件中 | |
<template> | |
<div class="section"> | |
<com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article> | |
<p>{{currentIndex}}</p> | |
</div> | |
</template> | |
<script> | |
import comArticle from './test/article.vue' | |
export default { name: 'HelloWorld', components: { comArticle }, data() { return { currentIndex: -1, articleList: ['红楼梦', '西游记', '三国演义'] } }, methods: { onEmitIndex(idx) { this.currentIndex = idx } }} | |
</script> | |
<template> | |
<div> | |
<div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div> | |
</div> | |
</template> | |
<script> | |
export default { props: ['articles'], methods: { emitIndex(index) { this.$emit('onEmitIndex', index) } }} | |
</script> |
另外:props同样可以使子组件向父组件传值:
父组件中::在子组件标签上绑定自定义属性 这个属性的值是父组件的一个函数: | |
1.----- <child :value='fn'></child> | |
子组件中: | |
2.--------props:['value']-----接收父组件传入的函数 | |
this.value(要传入父组件的值)------调用这个函数 把要传递的值作为形参 | |
父组件中 | |
3.-------- fn(接收到子组件传入的值){ | |
// 后续逻辑 | |
} |
二、 $children
/ $parent
上面这张图片是vue
官方的解释,通过$parent
和$children
就可以访问组件的实例,拿到实例代表什么?代表可以访问此组件的所有方法和data
。接下来就是怎么实现拿到指定组件的实例。
使用方法
// 父组件中 | |
<template> | |
<div class="hello_world"> | |
<div>{{msg}}</div> | |
<com-a></com-a> | |
<button @click="changeA">点击改变子组件值</button> | |
</div> | |
</template> | |
<script> | |
import ComA from './test/comA.vue' | |
export default { | |
name: 'HelloWorld', | |
components: { ComA }, | |
data() { | |
return { | |
msg: 'Welcome' | |
} | |
}, | |
methods: { | |
changeA() { | |
// 获取到子组件A | |
this.$children[0].messageA = 'this is new value' | |
} | |
} | |
} | |
</script> | |
// 子组件中 | |
<template> | |
<div class="com_a"> | |
<span>{{messageA}}</span> | |
<p>获取父组件的值为: {{parentVal}}</p> | |
</div> | |
</template> | |
<script> | |
export default { | |
data() { | |
return { | |
messageA: 'this is old' | |
} | |
}, | |
computed:{ | |
parentVal(){ | |
return this.$parent.msg; | |
} | |
} | |
} | |
</script> |
参考 前端进阶面试题详细解答
要注意边界情况,如在#app
上拿$parent
得到的是new Vue()
的实例,在这实例上再拿$parent
得到的是undefined
,而在最底层的子组件拿$children
是个空数组。也要注意得到$parent
和$children
的值不一样,$children
的值是数组,而$parent
是个对象注意: 通过$children拿到的子组件的数组集合 他们的下标是根据在父组件中子组件标签的书写顺序来的:<child1> <child1 /><child2> <child2 />
child1在child2的上面书写 那么父组件中使用this.$children0 得到的就是child1
总结
上面两种方式用于父子组件之间的通信, 而使用props进行父子组件通信更加普遍; 二者皆不能用于非父子组件之间的通信。
三、provide
/ inject
概念:
provide
/ inject
是vue2.2.0
新增的api, 简单来说就是父组件中通过provide
来提供变量, 然后再子组件中通过inject
来注入变量。
注意: 这里不论子组件嵌套有多深, 只要调用了inject
那么就可以注入provide
中的数据,而不局限于只能从当前父组件的props属性中回去数据
举例验证
接下来就用一个例子来验证上面的描述: 假设有三个组件: A.vue、B.vue、C.vue 其中 C是B的子组件,B是A的子组件
// A.vue | |
<template> | |
<div> | |
<comB></comB> | |
</div> | |
</template> | |
<script> | |
import comB from '../components/test/comB.vue' | |
export default { | |
name: "A", | |
provide: { | |
for: "demo" | |
}, | |
components:{ | |
comB | |
} | |
} | |
</script> | |
// B.vue | |
<template> | |
<div> | |
{{demo}} | |
<comC></comC> | |
</div> | |
</template> | |
<script> | |
import comC from '../components/test/comC.vue' | |
export default { | |
name: "B", | |
inject: ['for'], | |
data() { | |
return { | |
demo: this.for | |
} | |
}, | |
components: { | |
comC | |
} | |
} | |
</script> | |
// C.vue | |
<template> | |
<div> | |
{{demo}} | |
</div> | |
</template> | |
<script> | |
export default { | |
name: "C", | |
inject: ['for'], | |
data() { | |
return { | |
demo: this.for | |
} | |
} | |
} | |
</script> |
四、ref
/ refs
ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据, 我们看一个ref
来访问组件的例子:
// 子组件 A.vue | |
export default { | |
data () { | |
return { | |
name: 'Vue.js' | |
} | |
}, | |
methods: { | |
sayHello () { | |
console.log('hello') | |
} | |
} | |
} | |
// 父组件 app.vue | |
<template> | |
<component-a ref="comA"></component-a> | |
</template> | |
<script> | |
export default { | |
mounted () { | |
const comA = this.$refs.comA; | |
console.log(comA.name); // Vue.js | |
comA.sayHello(); // hello | |
} | |
} | |
</script> |
五、eventBus
eventBus
又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。
步骤:
1. 初始化
方式1:
首先需要创建一个事件总线并将其导出, 以便其他模块可以使用或者监听它.
// event-bus.js | |
import Vue from 'vue' | |
export const EventBus = new Vue() |
方式2:
// main.js | |
Vue.prototype.$bus=new Vue() // 在Vue的原型上挂载事件总线 | |
// 这种方式在使用事件总线的时候不需要在每个组件中导入bus, | |
// 使用this.$bus.emit('自定义事件1',要传入的值)传值 | |
// 使用this.$bus.$on('自定义事件1',(val)=>{})接收 val是传入的值 |
2. 发送事件
假设你有两个组件: additionNum
和 showNum
, 这两个组件可以是兄弟组件也可以是父子组件;这里我们以兄弟组件为例:
<template> | |
<div> | |
<show-num-com></show-num-com> | |
<addition-num-com></addition-num-com> | |
</div> | |
</template> | |
<script> | |
import showNumCom from './showNum.vue' | |
import additionNumCom from './additionNum.vue' | |
export default { | |
components: { showNumCom, additionNumCom } | |
} | |
</script> | |
// addtionNum.vue 中发送事件 | |
<template> | |
<div> | |
<button @click="additionHandle">+加法器</button> | |
</div> | |
</template> | |
<script> | |
import {EventBus} from './event-bus.js' | |
console.log(EventBus) | |
export default { | |
data(){ | |
return{ | |
num:1 | |
} | |
}, | |
methods:{ | |
additionHandle(){ | |
EventBus.$emit('addition', { | |
num:this.num++ | |
}) | |
} | |
} | |
} | |
</script> |
3. 接收事件
// showNum.vue 中接收事件 | |
<template> | |
<div>计算和: {{count}}</div> | |
</template> | |
<script> | |
import { EventBus } from './event-bus.js' | |
export default { | |
data() { | |
return { | |
count: 0 | |
} | |
}, | |
mounted() { | |
EventBus.$on('addition', param => { | |
this.count = this.count + param.num; | |
}) | |
} | |
} | |
</script> |
4. 移除事件监听者
如果想移除事件的监听, 可以像下面这样操作:
import { eventBus } from 'event-bus.js' | |
EventBus.$off('addition', {}) |
事件总线的两个问题:
- 问题1: 为什么第一次触发的时候页面B中的on事件没有被触发
- 问题2: 为什么后面再一次依次去触发的时候会出现,每一次都会发现好像之前的on事件分发都没有被撤销一样,导致每一次的事件触发执行越来越多。
问题一
第一次触发的时候页面B中的on事件没有被触发
产生原因
当我们还在页面A的时候,页面B还没生成,也就是页面B中的 created中所监听的来自于A中的事件还没有被触发。这个时候当你A中emit事件的时候,B其实是没有监听到的。
解决办法
我们可以把A页面组件中的emit事件写在beforeDestory中去。因为这个时候,B页面组件已经被created了,也就是我们写的on事件已经触发了,所以可以在beforeDestory的时候, on事件已经触发了,所以可以在beforeDestory的时候,on事件已经触发了,所以可以在beforeDestory的时候,emit事件
问题二
后面再一次依次去触发的时候会出现,每一次都会发现好像之前的on事件分发都没有被撤销一样,导致每一次的事件触发执行越来越多。
产生原因
就是说,这个$on事件是不会自动清楚销毁的,需要我们手动来销毁。(不过我不太清楚这里的external bus 是什么意思,有大神能解答一下的吗,尤大大也提到如果是注册的是external bus 的时候需要清除)
解决办法
在B组件页面中添加Bus.$off来关闭
// 在B组件页面中添加以下语句,在组件beforeDestory的时候销毁。 | |
beforeDestroy () { | |
bus.$off('get', this.myhandle) | |
}, |
总结
所以,如果想要用 bus 来进行页面组件之间的数据传递,需要注意两点:
一、组件A emit 事件应在beforeDestory生命周期内。
二、组件B内的 on 记得要销毁
六、Vuex
1. Vuex介绍
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化. Vuex 解决了多个视图依赖于同一状态
和来自不同视图的行为需要变更同一状态
的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上
2. Vuex各个模块
state
:用于数据的存储,是store中的唯一数据源getters
:如vue中的计算属性一样,基于state数据的二次包装,常用于数据的筛选和多个数据的相关性计算mutations
:类似函数,改变state数据的唯一途径,且不能用于处理异步事件actions
:类似于mutation
,用于提交mutation
来改变状态,而不直接变更状态,可以包含任意异步操作modules
:类似于命名空间,用于项目中将各个模块的状态分开定义和操作,便于维护
3. Vuex实例应用
// 父组件 | |
<template> | |
<div id="app"> | |
<ChildA/> | |
<ChildB/> | |
</div> | |
</template> | |
<script> | |
import ChildA from './components/ChildA' // 导入A组件 | |
import ChildB from './components/ChildB' // 导入B组件 | |
export default { name: 'App', components: {ChildA, ChildB} // 注册A、B组件 | |
} | |
</script> | |
// 子组件childA | |
<template> | |
<div id="childA"> | |
<h1>我是A组件</h1> | |
<button @click="transform">点我让B组件接收到数据</button> | |
<p>因为你点了B,所以我的信息发生了变化:{{BMessage}}</p> | |
</div> | |
</template> | |
<script> | |
export default { data() { return { AMessage: 'Hello,B组件,我是A组件' | |
} }, computed: { BMessage() { // 这里存储从store里获取的B组件的数据 | |
return this.$store.state.BMsg | |
} }, methods: { transform() { // 触发receiveAMsg,将A组件的数据存放到store里去 | |
this.$store.commit('receiveAMsg', { AMsg: this.AMessage | |
}) } } } | |
</script> | |
// 子组件 childB | |
<template> | |
<div id="childB"> | |
<h1>我是B组件</h1> | |
<button @click="transform">点我让A组件接收到数据</button> | |
<p>因为你点了A,所以我的信息发生了变化:{{AMessage}}</p> | |
</div> | |
</template> | |
<script> | |
export default { data() { return { BMessage: 'Hello,A组件,我是B组件' | |
} }, computed: { AMessage() { // 这里存储从store里获取的A组件的数据 | |
return this.$store.state.AMsg | |
} }, methods: { transform() { // 触发receiveBMsg,将B组件的数据存放到store里去 | |
this.$store.commit('receiveBMsg', { BMsg: this.BMessage | |
}) } } } | |
</script> |
vuex的store,js
import Vue from 'vue' | |
import Vuex from 'vuex' | |
Vue.use(Vuex) | |
const state = { | |
// 初始化A和B组件的数据,等待获取 | |
AMsg: '', | |
BMsg: '' | |
} | |
const mutations = { | |
receiveAMsg(state, payload) { | |
// 将A组件的数据存放于state | |
state.AMsg = payload.AMsg | |
}, | |
receiveBMsg(state, payload) { | |
// 将B组件的数据存放于state | |
state.BMsg = payload.BMsg | |
} | |
} | |
export default new Vuex.Store({ | |
state, | |
mutations | |
}) |
七、vue slot 插槽通信
父子插槽通信
可以理解为在定义组件的时候预先留好了一个插槽,父组件在调用子组件的使用将东西插到插槽里面显示,或者说从外向里读。
//父组件 | |
<div> | |
<h3>父组件</h3> | |
<testChild> | |
<div>默认插槽</div> | |
</testChild> | |
</div> | |
//子组件 testChild | |
<div> | |
<h4>子组件</h4> | |
<slot></slot> //default slot | |
</div> |
结果如下
父组件
子组件
默认插槽
父向子通信
其实就是读取父里面data的内容
/* 父组件 | |
* list: [ | |
* {name: "aaa"}, {name: "bbb"} | |
* ] | |
*/ | |
<div> | |
<h3>父组件</h3> | |
<testChild> | |
<template v-slot:test>//v-slot: + 插槽名 v-slot:default 也是允许的 | |
<ul> | |
<li v-for="item in list">{{item.name}}</li> | |
</ul> | |
</template> | |
</testChild> | |
</div> | |
//子组件 textChild | |
<div> | |
<h4>子组件</h4> | |
<slot name="test"></slot> //name="插槽名" | |
</div> |
结果如下
父组件
子组件
- aaa
- bbb
子向父通信
父组件无法直接访问子组件里面的变量
//父组件 | |
<div> | |
<h3>父组件</h3> | |
<testChild> | |
<template v-slot:test="data">//具名插槽,v-slot: +插槽名+ ="自定义数据名",子组件所传参数都是其属性 | |
<ul> | |
<li v-for="item in data.list2">{{item.name}}</li> | |
</ul> | |
</template> | |
<template v-slot="dataDefalut">//默认插槽 | |
{{dataDefalut.sName}} | |
</template> | |
</testChild> | |
</div> | |
//子组件 | |
<template> | |
<div> | |
<h4>子组件</h4> | |
<slot name="test" :list2="list2"></slot> | |
<slot :sName="name"></slot> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: "testChild", | |
data(){ | |
return { | |
list2:[ | |
{name:'ccc'}, | |
{name:'ddd'} | |
], | |
name:'name' | |
} | |
} | |
} | |
</script> |