前言
最近在网上发现面试官都很喜欢问vue
组件间的通信方式有哪些,问问应届生也就算了,1年工作经验也问,勉勉强强那也还说得过去吧!到了3年经验的你还问啊?算了,问就问吧,可能3年也有些水分。到了5年经验,人家都要求15k+的人了,你还问啊?好好好!这么玩是吧,那就一次给你说完吧!
父子组件双向通信之props/emit
下面我将通过给自定义组件使用v-model
和.sync
修饰符,展示父子组件间如何通过的双向数据通信,并且还能顺便复习一下v-model
和.sync
是如何实现双向数据绑定。
1.v-model(vue2.2.0+)
v-model
默认用法
在子组件上绑定v-model
,子组件内通过props
的value
属性接收值,并通过触发$emit('input', newValue)
更新父组件中的值,实现数据双向绑定。
Parent.vue
js
<Child v-model="name"></Child>
Child.vue
js
<template>
<div class="child">
<input :value="value" @input="$emit('input', $event.target.value)">
</div>
</template>
<script>
export default {
props: {
value: String
}
}
</script>
自定义接收v-model
的属性名及触发事件名
当子组件已经使用了props
的value
属性作为其他用途,这时你再使用v-model
时,就可以自定义接收v-model
值的属性名,我们可以通过和props
同级的model
属性配置相关信息。下面的例子展示了其用法:
Parent.vue
js
<template>
<div id="app">
<Child v-model="name" :value="age"></Child>
</div>
</template>
<script>
import Child from './components/Child.vue'
export default {
components: {
Child
},
data() {
return {
name: '蔡徐坤',
age: 18
}
}
}
</script>
Child.vue
js
<template>
<div class="child">
年龄:{{ value }}
姓名:<input :value="vModelValue" @input="$emit('valueChange', $event.target.value)">
</div>
</template>
<script>
export default {
model: {
// 自定义props中接收v-model的属性名
prop: 'vModelValue',
// 自定义触发更新父组件v-model值的事件名
event: 'valueChange'
},
props: {
value: Number,
// v-model传递的值和model配置的名称一致
vModelValue: String
}
}
</script>
2.修饰符.sync(vue2.3.0+)
父组件向子组件传递props
属性时,可在属性名后增加.sync
修饰符,如:name.sync="name"
,子组件可通过触发$emit(upadte:name, newValue)
事件更新父组件中的值。
看到这是不是觉得.sync
特别像vue3
当中的v-model:name="name"
用法。没错,就是一样的用法!
Parent.vue
js
<template>
<div id="app">
<Child :name.sync="name"></Child>
</div>
</template>
Child.vue
js
<template>
<div class="child">
姓名:<input :value="name" @input="$emit('update:name', $event.target.value)">
</div>
</template>
<script>
export default {
props: {
name: String
}
}
</script>
attrs/listeners(2.4.0+)跨组件双向通信
这两个api
可以实现透传,在创建高级别的组件时非常有用。比如我们现在再来一个孙子 组件,而我们的儿子 组件特别懒,它让父亲 有什么事情直接和孙子说 。这时我们就可使用透传,实现跨组件通信:
Parent.vue
js
<template>
<div class="parent">
<Son :money="money" @cb="cb"></Son>
</div>
</template>
<script>
import Son from './components/Son.vue'
export default {
components: {
Son
},
data() {
return {
money: '100万'
}
},
methods: {
// 孙子告诉爷爷收到了多少钱,防止被中间商赚差价
cb(money) {
console.log('孙子收到了:', money)
}
},
}
</script>
Son.vue
js
<template>
<div class="son">
<Grandson v-bind="$attrs" v-on="$listeners"></Grandson>
</div>
</template>
<script>
import Grandson from './Grandson.vue'
export default {
components: {
Grandson
}
}
</script>
Grandson.vue
js
<template>
<div class='grandson'>
太开心了 ,爷爷直接给了我{{money}}!
<button @click="$emit('cb', money)">点击,通知爷爷收到多少钱!</button>
</div>
</template>
<script>
export default {
props: {
money: String
}
}
</script>
这样我们就轻松实现了一个跨组件的双向通信。
$refs
$refs
是一个对象,持有注册过 ref
attribute 的所有 DOM 元素和组件实例。
我们可以给子组件绑定一个ref
名称,就能够通过this.$refs[ref名称]
,获取到对应的组件实例。从而能够操作子组件中的属性或方法。
Parent.vue
js
<Son ref="Son"></Son>
Son.vue
js
<Grandson ref="Grandson"></Grandson>
以上爷爷给儿子挂了ref
,儿子又给孙子挂了ref
,这时爷爷也可以通过儿子的ref
间接获取到孙子的实例(this.$refs.Son.$refs.Grandson
)。
所以我们也可看出$refs
也能实现跨组件的通信。
chidren/parent/$root
$chidren:当前实例的直接子组件。
$parent:父实例,如果当前实例有的话。
$root:当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
我们还是拿上面的Parent.vue
、Son.vue
、Grandson.vue
三个组件来讲。
Parent.vue
js
<template>
<div class="parent">
<Son ref="Son"></Son>
</div>
</template>
<script>
...
export default {
...
mounted() {
console.log(this.$children) // [Son实例]
console.log(this.$refs.Son === this.$children[0]) // true
console.log(this.$parent) // Parent实例
console.log(this.$root) // Parent实例
console.log(this === this.$parent) // false
console.log(this === this.$root) // false
console.log(this.$el === this.$parent.$el) // true
console.log(this.$el === this.$root.$el) // true
console.log(this.name) // 爷爷
console.log(this.$parent.name) // undefined
console.log(this.$root.name) // undefined
}
}
</script>
以上我们能够通过this.$children
获取到子组件的实例,也就是和$refs
获取到的实例是一样的,这样我们就能够建立与祖孙组件之间的通信。这里有一个注意点是,当组件自身没有父实例时,$parent
和$root
返回的是自身实例,但又不完全相等。
Son.vue
js
<template>
<div class="son">
<Grandson ref="Grandson"></Grandson>
</div>
</template>
<script>
export default {
...
mounted() {
console.log(this.$children) // [Grandson实例]
console.log(this.$parent) // Parent实例
console.log(this.$root) // Parent实例
}
}
</script>
Grandson.vue
js
<script>
export default {
mounted() {
console.log(this.$children) // []
console.log(this.$parent) // Son实例
console.log(this.$root) // Parent实例
}
}
</script>
以上我们看出结合$chidren/$parent/$root
三者也可以进行跨组件通信,原理和$refs
基本一致。
$slot插槽
没想到吧!插槽也能实现跨组件通信,我们下面就来实现一个
Parent.vue
向子组件传入插槽
js
<template>
<div class="parent">
<Son ref="Son">
<template #default="{ data }">
<button @click="printName(data.name)">输出孙子名字</button>
<div>
{{ data.name }}
</div>
</template>
</Son>
</div>
</template>
<script>
import Son from "./components/Son.vue";
export default {
components: {
Son,
},
methods: {
printName(name) {
console.log(name); // 孙子
}
}
};
</script>
Son.vue
把父组件的插槽继续传入下级子组件
js
<template>
<div class="son">
<Grandson ref="Grandson">
<template #default="props">
<slot v-bind="props"></slot>
</template>
</Grandson>
</div>
</template>
<script>
import Grandson from './Grandson.vue'
export default {
components: {
Grandson
}
}
</script>
Grandson.vue
把组件自身实例name
属性通过插槽的data
属性传递出去,上级组件可通过插槽名称<template default="{data}">
佬来解构data
属性。
js
<template>
<div class='grandson'>
<slot :data="{name}"></slot>
</div>
</template>
<script>
export default {
data() {
return {
name: '孙子'
}
}
}
</script>
以上我们向子组件注入了一个插槽,并且在这个插槽上绑定了一个父组件的事件来获取插槽内传递出来的数据,在子组件中又将这个插槽顺势传递到了孙子组件,这样进行爷孙俩的跨组件通信了!
provide/inject
这两个api
需要搭配使用,使用provide
可以向下级的所有组件提供依赖。在下级组件中,可通过inject
注入由所有上层组件通过provide
提供的依赖。但是需要注意,它们之间的绑定并不是响应式的,但是我们可以通过传入一个响应式对象,使它们变成可响应的。
provide
:一个对象或者返回一个对象的函数
inject
:字符串数组或者对象
普通用法:
js
// 祖先组件
provide: {
name: '哈哈哈'
}
// 下级组件
inject: ['name']
provide
使用对象的方式提供不能获取到this
实例,提供响应式对象。
进阶用法
js
// 祖先组件
provide() {
return {
name: this.name,
};
},
data() {
return {
name: "爷爷",
};
},
// 下级组件
inject: {
// 别名
foo: {
// 接收provide提供的name属性值
from: 'name',
// 默认值
default: '儿子'
}
}
这样子访问到this
实例了,但是还有一个问题是name
并不是响应式的,当name
值改变时,子孙组件并不会触发更新。这时我们可以通过在把name
包裹在一个对象里,再把这个对象传递下去就可实现响应式了。
js
// 祖先组件
provide() {
return {
name: this.provideData
};
},
data() {
return {
provideData: {
name: '爷爷'
}
};
},
高级用法
除了传递以上响应式对象外,还可以通过提供一个get
方法来获取祖先组件对应的属性值,不用包装对象也能做到响应式更新。因为通过provide
通过的方法会默认绑定当前组件实例的this
,就算没有绑定,我们也能通过方法.bind(this)
来手动绑定。
js
// 祖先组件
provide() {
return {
getName: this.getName
};
},
data() {
return {
name: '爷爷',
};
},
methods() {
getName() {
return this.name
}
}
// 下级组件
inject: {
getName: {
default: () => (() => {})
}
}
eventBus事件
eventBus
就是我们所称的事件总线,使用方式是新建一个Vue
实例并导出,可在任何组件当中导入,实现跨组件通信。
eventBus.js
js
import Vue from 'vue'
export const eventBus = new Vue()
在组件当中使用
js
// 监听事件
eventBus.$on('change', (a, b) => {
console.log(a, b)
})
// 触发事件
eventBus.$emit('change', 1, 2)
// 移除事件
eventBus.$off('change', this.onChange)
// 移除所有事件
eventBus.$off()
注意:记得事件监听使用后或者组件销毁前手动移除事件,否则事件监听将一直存在。
大杂烩通信
除了以上vue
内置的组件间通信方式外,还可以通过全局状态管理插件,如vuex/pinia
实现跨组件通信,当然还有本地缓存方式,如:localstorage/sessionStorage
等,好了这个问题就讲这么多了,面试结束了吗?
面试官:一个问题讲这么久?说完了吗?回去等通知吧!
结语
大家还有哪些手段,尽管在评论区使出来吧!