Vue 组件通信方式总结和组件通讯深入
组件通信方法:
父子通信:
props
$parent / $children (返回数组,需要遍历)
$attrs/$listeners
$ref 访问组件实例 (this.$refs.xxx.data)
provide / inject
子向父: 自定义事件 ( 子组件通过$emit触发自定义事件,父使用$on接收)
兄弟通信: Bus;Vuex
跨级通信: Bus;Vuex;provide&inject 、 $attrs & $listeners
第一种: props
书写方式: [todos] {todos:Array} {todos:{type:Arrat,default:[]}}
如果父组件给子组件传递数据:
- 为函数: 本质其实就是子组件给父组件传递数据
- 为数据: 本质其实就是父组件给子组件传递数据
适用场景:
-
父向子
父组件通过
v-bind
属性绑定向子组件共享数据,子组件使用props
接收数据。 -
子向父 (要求父先给子一个函数)
// 父组件 <Child :getData="getData"></Child> data() { return { get: '' } }, getData(event) { this.get = event }
// 子组件 <template> <div> <button @click="emitEvent">点击获取</button> {{ one }} </div> </template> <script> export default { name: 'Child', props: ['getData'], data() { return { one: 'data data data' } }, methods: { emitEvent() { this.getData(this.one) } } } </script>
注意:props的值如果是
对象类型的值
,修改时不会报错
,不能使用v-model绑定props
的值,因为props的值不可以修改!
第二种: 自定义事件
子组件通过 e m i t , 父 组 件 通 过 emit ,父组件通过 emit,父组件通过on接收
书写方式:
两种【简单写法|复杂写法】
<Todos @erha="handler">
<Todos ref='erha'>
mounted(){
this.$refs.erha.$on('xx',callBack)
}
第三种: 全局事件总线 $bus
该方法通过Vue实例对象作为中央事件总线,用她来触发事件和监听事件,巧妙得实现了任何组件之间的通信。
使用:
// 挂载全局事件总线
Vue.prototype.$bus = this;
适用场景:万能(兄弟、父子、任何组件都可以实现通信,`但通常用于兄弟组件传值`)
// 发送方组件
<span>{{elementValue}}</span>
<input type="button" value="点击触发" @click="elementByValue">
<script>
export default {
data () {
return { elementValue: 4 }
},
methods: {
elementByValue() {
Bus.$emit('val', this.elementValue)
}
}
}
</script>
// 接收方
<input type="button" value="点击触发" @click="getData">
<span>{{name}}</span>
<script>
export default {
data () {return {name: 0 }},
mounted () {
// 用$on事件来接收参数
Bus.$on('val', (data) => {
this.name = data
})
},
methods: {
getData: function () {
this.name++
}
}
}
</script>
第四种:Vuex 全局组件共享
第五种:slot插槽 适用于父子组件通信 (dom结构)
解析 Vue实例对象
$root 根实例
$options 每一个Vue实例都有一个实例对象 options
$children 子组件实例 (是一个数组)
$parent 父组件实例
$listeners 获取到父组件给子组件的传递的自定义事件
$emint 子组件抛出的自定义事件
$on 通过on接收自定义事件
$attrs和$props 加起来才是所有子组件的所有自定义属性:
$attrs 能获取到子组件所有未被props接收的属性 (排除了$props、class、style以外的其他属性,极端情况下使用,注意不能直接用模板字符串使用$attrs的数据)
$props 获取到子组件的所有props,父子传参时使用,正常情况下都用prpos传参,因为是响应式的数据也更安全。
$data 拿到组件完整的data对象
$refs 如果是HTML标签,拿到的是dom对象,如果是组件标签,拿到的是组件实例对象(从而操作组件的数据和方法)
$vnode 当前组件的虚拟节点
$router 拿到VueRouter实例
$route 获取组件的路由信息(name、meta、path、query等参数)
注意: App.vue 并不是根实例 ,是`$root根实例`的子组件。
1. event 事件对象
<button @click="handler">第一个原生btn按钮</button>
<Event1 @click"handler1"></Event1>
<Event1 @click.native="handler1"></Event1>
<Event2 @click="handler2" @xxx="handler2"></Event2>
<!-- 自定义事件对于原生DOM没有任何意义 因为没有办法触发$emit函数 -->
<!-- <button @erha="handler3"> 原生的btn</button> -->
<input type="week" />
methods: {
//原生DOM事件的回调
handler(event){
console.log('原生dom的click事件')
console.log(event);
},
handler1(){
console.log('66666666');
},
handler2(params){
console.log(params);
}
}
当我们点击 第一个原生btn按钮
时,控制台会输出 event 对象,但是点击第一个组件时就无法触发,因为组件上的原生dom事件会被默认当做自定义事件,要想在组件标签上触发原生dom事件,需要加入.native
修饰符,native这个词就是原生的意思, 他的作用就是 把自定义事件变为 原生DOM事件
。
2. v-model 深入
我们通常使用
v-model
指令用于收集表单数据 [text
、radio
、checkbox
、range
] 等等,进行数据的双向绑定和展示,注意v-model 收集checkbox需要用数组收集。
父组件:
<input type="text" v-model="msg">
<span>{{msg}}</span>
<!-- 原生DOM当中是有oninput事件:当表单元素发生文本的变化的时候就会立即出发 -->
<!-- 原生DOM:通过 value与input事件实现 v-model 功能-->
<input type="text" :value="msg" @input=" msg = $event.target.value"/>
<span>{{msg}}</span>
<!-- 并非原生DOM: 在组件中实现父子组件通信和数据同步-->
<!-- :value 是什么? 是一个props,用于实现父子组件通信
@input 是什么? 并非原生DOM的 input 事件,而是一个自定义事件
-->
<CustomInput :value="msg" @input="msg = $event"></CustomInput>
<!-- 简化写法,和上一行代码效果一样 -->
<CustomInput v-model="msg"></CustomInput>
<script type="text/ecmascript-6">
import CustomInput from './CustomInput.vue'
export default {
name: 'ModelTest',
data() {
return {
msg:"我爱塞北的大雪呀"
}
},
components: {
CustomInput
}
}
</script>
CustomInput 子组件:
<template>
<div style="background: #ccc; height: 50px;">
<h2>input包装组件----{{value}}</h2>
<!-- 这里的 :value 和 @input 是什么?
:value 动态属性
@input 给原生dom绑定原生dom事件 -->
<input :value="value" @input="$emit('input',$event.target.value)"/>
</div>
</template>
<script type="text/ecmascript-6">
export default {
name: 'CustomInput',
props:['value']
}
</script>
注意: 在上面代码中input 使用了 :value=“msg”,是用于单向数据绑定,也就是说输入框会默认显示data中msg的值,而在 CustomInput 组件中使用的 :value="msg"是
props
总结:
// v-model:实现原理 :value @input 还可以实现父子数据同步。
<CustomInput v-model="msg"></CustomInput>
3. 使用属性修饰符 .sync 实现父子组件同步
【案例】: 小明的爸爸有多少钱 ?
父组件:
<template>
<div>
小明的爸爸现在有{{ money }}元
<h2>不使用sync修改符</h2>
<!-- 以前我们写自定义事件都是写一个名字,这里的 @update:money 中的money就是props中的money
因为我们这里实现逻辑简单,没有写相应的回调函数,
这里只需要让money的值更新一下(子组件自定义事件传回来的数据)-->
<Child :money="money" @update:money="money = $event"></Child>
<h2>使用sync修改符</h2>
<!-- :money.sync的含义:
第一: 父组件给子组件传递props,名为money
第二: 给当前子组件绑定了一个自定义事件,而且事件名称即为 @update:money"-->
<Child :money.sync="money"></Child>
</div>
</template>
<script >
import Child from './Child.vue'
export default {
name: 'SyncTest',
data() {
return {
money: 10000
}
},
components: {
Child,
Child2
}
}
</script>
子组件 Child:
<template>
<div style="background: #ccc; height: 50px;">
<!-- 触发自定义事件,第一个参数是触发自定义事件的名字 update:money,
第二个参数是子组件需要把父亲还剩多少钱告诉父亲 -->
<button @click="$emit('update:money',money - 100)">点击花费100元</button>
爸爸还剩 {{money}} 元
</div>
</template>
<script type="text/ecmascript-6">
export default {
name: 'Child',
props:['money']
}
</script>
4. a t t r s 与 attrs与 attrs与listeners 实现父子组件通信
多级组件嵌套需要传递数据时,通常使用的方法是通过vuex。但如果仅仅是传递数据,而不做中间处理,使用 vuex 处理,未免有点大材小用。为此 Vue2.4 版本提供了另一种方法
$attrs
/$listeners
。
$attrs
:组件实例的属性,能获取到子组件所有未被props接收的属性
$listeners
:组件实例的属性,可以获取到父亲传递自定义事件(对象形式呈现)
【案例】:基于 element-ui 实现自定义带 Hover 提示的按钮:
父组件:
<template>
<div>
<h2>自定义带Hover提示的按钮</h2>
<!-- 二次封装的HintButton按钮的时候,把人家el-button需要的数据传递过去 -->
<!-- 注意这里的@click 代表自定义事件而非原生dom的click事件 -->
<HintButton type="success" icon="el-icon-plus" title="我是中国人" @click="handler"/>
</div>
</template>
<script >
import HintButton from './HintButton';
export default {
name: 'AttrsListenersTest',
components:{
HintButton
},
methods: {
handler() {
alert('点击按钮时弹出的自定义事件!');
},
},
}
</script>
子组件:
<template>
<div>
<!-- 可以巧妙的利用 a标签的title属性,实现按钮带有提示功能 -->
<a :title="title">
<!-- 下面这行注释的代码写得很呆,如果有很多属性的话一个个接收太麻烦了-->
<!-- <el-button :type="$attrs.type" :icon="$attrs.icon" v-on="$listeners">添加</el-button>-->
<!-- 我们可以直接使用`v-bind="$attrs"` 但是不能简写成 :、 v-on也不能简写-->
<el-button v-bind="$attrs" v-on="$listeners">添加</el-button>
</a>
</div>
</template>
<script>
export default {
name: "",
props:['title'],
mounted(){
// this.$attrs:可以获取到父亲传递的属性 (排除了$props、class、style以外的其他属性)
// 打印后发现: this.$attrs 是获取不到的 title 的,因为title已经通过 props 接收
console.log(this.$attrs);
// $listeners 获取到父组件给子组件的传递的自定义事件(也是组件实例自身的属性)
console.log(this.$listeners);
}
};
</script>
<style scoped></style>
5. $ refs 、 c h i l d r e n 、 children、 children、parent 实现父子组件通信
$refs:父组件访问子组件
如果在普通的DOM元素上使用,引用指向的是DOM元素;
如果用在子组件上,引用的是组件实例
$children:父组件访问子组件 (如果是多次的$refs操作,我们可以使用$children属性)
$parent: 子组件访问父组件
【案例】:父亲向孩子借钱,动态展示父亲和孩子们的剩余存款
父组件:
<template>
<div>
<h2>父亲有存款: {{ money }}</h2>
<button @click="JieQianFromXM(100)">找小明借钱100</button><br />
<button @click="JieQianFromXH(150)">找小红借钱150</button><br />
<button @click="JieQianAll(200)">找所有孩子借钱200</button><br />
<button @click="SendInfo">我是baba</button>
<br />
<!-- 小明 -->
<Son ref="xm" />
<br />
<!-- 小红 -->
<Daughter ref="xh"/>
</div>
</template>
<script>
import Son from "./Son";
import Daughter from "./Daughter";
export default {
name: "ChildrenParentTest",
data() {
return {
money: 1000,
};
},
methods: {
//找儿子借钱
JieQianFromXM(money) {
this.money += money; // 点击按钮父亲存款每次 +100
// $ref 可以获取真实dom节点,也可以获取子组件标签实例对象,从而操作组件的数据和方法
this.$refs.xm.money -= money; // 儿子存款每次 -100
},
JieQianFromXH(money) {
//父组件的数据累加150
this.money += money;
this.$refs.xh.money -= money;
},
JieQianAll(money){
this.money += 2 * money; // 父亲向所有孩子借钱,得到二倍money
// 不使用 $fefs,使用 $children 一样可以获取子组件的实例对象 [返回的是一个数组]
this.$children.forEach(item=>item.money-=money);
//不建议用枚举获取子组件:因为组件过多时,没办法确定到底是那个子组件
// this.$children[0].money -=money;
},
SendInfo(){
//在父组件中获取到子组件(数据+方法)
this.$refs.xm.tinghua();
}
},
components: {
Son,
Daughter,
},
};
</script>
子组件小明 Son.vue :
<template>
<div style="background: #ccc; height: 50px;">
<h3>孩子小明: 有存款: {{money}}</h3>
<button @click="giveMoney(50)">给父亲50元</button>
</div>
</template>
<script>
export default {
name: 'Son',
data () {
return {
money: 30000
}
},
methods: {
tinghua(){
console.log('我是小明,我听爸爸的话');
},
// 儿子给父亲钱的回调函数
giveMoney(money){
this.money-=money; // 儿子 -50
// 在子组件内部获取父亲的存款再 +50
this.$parent.money += money;
}
}
}
</script>
子组件小红 Daughter.vue :
<template>
<div style="background: #ccc; height: 50px;">
<h3> 孩子小红: 有存款: {{money}}</h3>
<button @click="giveMoney(666)">给父亲666元</button>
</div>
</template>
<script>
export default {
name: 'Daughter',
data () {
return {
money: 20000
}
},
methods: {
giveMoney(money){
this.money-=money;
this.$parent.money += money;
}
}
}
</script>
总结:
$ref
: 可以在父组件内部获取子组件—实现父子通信
$children
: 可以在父组件内部获取全部的子组件【返回数组】
$parent
: 可以在子组件内部获取唯一的父组件【返回组件实例】
6. 通过 provide/inject 轻松实现跨级访问祖先组件的数据
使用方法:provide
在父组件中返回要传给下级的数据,inject
在需要使用这个数据的子辈组件或者孙辈等下级组件中注入数据。
// 父组件通过provide将自己的数据以对象形式传出去
provide(){
return {
parentValue:"我是父组件的值啊"
}
}
// 子孙组件接受方式:
// inject:["parentValue"], // 使用一个注入的值作为数据入口:
inject:{
// 使用一个默认值使其变成可选项
parentValue: { // 健名
from: 'parentValue', // 来源
default: 'parentValue' // 默认值
}
}
注意:provide并不是响应式的,当子组件inject的时候已经丢失了响应式功能。
如实现父组件与子孙组件所绑定的数据动态响应呢?
-------------------父辈组件----------------------
provide(){
return {
// keyName: {name:this.name}, // value 是对象才能实现响应式,也就是引用类型
keyName: this.changeValue // 通过函数的方式也可以[注意,这里是把函数作为value,而不是this.changeValue()]
// keyName: 'test' value 如果是基本类型,就无法实现响应式
}
},
data(){
return {
name:'张三'
}
},
methods: {
changeValue(){
this.name = '改变后的名字-李四'
}
}
-------------子、孙辈组件-----------------
inject:['keyName']
create(){
console.log(this.keyName) // 改变后的名字-李四
}
当我们以如上的形式书写代码的时候,其实相当于对
this.changeValue
做了一层浅拷贝,虽然inject的时候已经丢失了响应式功能。但由于仅仅是浅拷贝,所以响应式对象的属性仍旧是响应式的。
依照这个思路,如果我们希望整个数据都是响应式的。那么可以通过一下的方法实现:
-------------------父辈组件----------------------
provide() {
return {
reactiveMsg: () => this.msg
}
},
-------------子、孙辈组件-----------------
{
inject: ['reactiveMsg'],
computed: {
computedProperty() {
return this.reactiveMsg()
}
},
watch: {
computedProperty() {
console.log('数据改变')
}
}
}
如上
provide
提供一个函数。函数内部返回一个响应式的数据。此时整条数据的响应式的状态并不会丢失。
而且这样做有一个好处,即无法直接修改computedProperty
的值,因为他是一个计算属性。这样就可以避免数据的混乱。
【实例】:让所有组件共享 App.vue的数据和方法
provide() {
return {
app:this
}
}
接下来,任何组件只要通过inject:['app']
,都可以通过this.app.xxx的形式来访问App.vue的data,computed,method等内容。
使用场景:
-
比如在App.vue中我们调用了获取用户信息的接口,其他页面需要使用这个方法。
-
刷新vue组件
------------------app.vue----------------------- <template> <div id="app"> <!-- isRouterAlive 保证keep-alive时,不影响全局刷新 --> <div v-if="isRouterAlive"> <keep-alive> <router-view v-if="$route.meta.keepAlive"> <!-- 这里是会被缓存的视图组件,比如列表A页面 --> </router-view> </keep-alive> <router-view v-if="!$route.meta.keepAlive"> <!-- 这里是不被缓存的视图组件,比如详情B页面--> </router-view> </div> </div> </template> <script> export default { name: 'App', components: { MergeTipDialog, BreakNetTip }, data () { return { isShow: false, isRouterAlive: true }, // 父组件中返回要传给下级的数据 provide () { return { reload: this.reload } }, methods: { reload () { this.isRouterAlive = false this.$nextTick(() => { this.isRouterAlive = true }) } } } </script>
------------------子孙组件----------------------- <template> <popup-assign :id="id" @success="successHandle" > <div class="confirm-d-tit"><span class="gray-small-btn">{{ name }}</span></div> <strong>将被分配给</strong> <a slot="reference" class="unite-btn" > 指派 </a> </popup-assign> </template> <script> import PopupAssign from '../PopupAssign' export default { //引用vue reload方法 inject: ['reload'], components: { PopupAssign }, methods: { // ...mapActions(['freshList']), async successHandle () { this.reload() } } } </script>
solt 插槽
作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于父➡子组件通信。
作用域插槽:子组件的数据来源于父组件,子组件决定不了自身结构和外观。
通过props实现父子传值时为什么传入数字也需要使用 v-bind 呢 ?
为了告诉 Vue,传入的是 这是一个 JavaScript 表达式,如果不使用v-bind 将会被解析成一个字符串。 正确写法:
<Son :likes="42"/>