最近一直在西安找工作,记录一下我常碰见的面试题吧,顺便:求内推!!!
面试官:Vue组件通讯都有哪些方法。
这种题应该属于最简单的Vue八股文了,我们一个点一个点开始解析。先说一下答案,由于现在我基本上以vue3为主,所以示例代码都是vue3写法
Props
$emit
事件触发Ref
模版引用$parent
和$root
attrs
和listeners
provide
和inject
- Vuex 或者 Pinia
- 本文中主要讨论的EventBus
答案解析
Props
props就是最基础的父子组件传值,由父组件通过v-bind
或者:
直接给子组件,子组件接收的时候最好限制一下类型,做一些判断。我写个基本demo吧
- 父组件
<template>
<child :b="b"/>
</template>
<script setup>
const b = ref(1)
</script>
- 子组件
<template>
<p>{{props.b}}</p>
</template>
<script setup>
const props = defineProps({
b:{
type: Number
}
})
</script>
$emit
$emit
一般用于子组件向父组件传值,由父组件通过v-on
或者@
来监听子组件的事件,子组件则通过 $emit
触发这个事件来通讯。
- 父组件
<template>
<child @test="test" />
</template>
<script setup>
const test = (a)=>{
console.log(a)
}
</script>
- 子组件
<script setup>
const emits = defineEmits(['test'])
emits('test','test emited')
</script>
Ref
模版引用
这里的ref
不是指响应式变量,而是获取子组件的实例或者是子组件的虚拟DOM。获取到子组件的实例,就可以直接修改值或者调用方法,这样也实现了通讯,但是不推荐这种方式,因为破坏了单向数据流。
<template>
<child ref="child" />
</template>
<script setup>
const child=ref(null)
onMounted(()=>{
console.log(child.value.a)
child.value.test('2')
console.log(child.value.a)
})
</script>
- 子组件
<template>
<p> {{ a }}</p>
</template>
<script setup>
const a = ref(1)
const test = (val)=>a.value=val
defineExpose({a,test})
</script>
$parent
和 $root
在vue2还有vue3的optionsAPI中,我们可以直接使用this.$parent
和 this.$root
,但是在vue3的组合式API中貌似没有这个了,但是我们可以通过getCurrentInstance
来获取到,但是这种方式依旧不推荐,因为破坏了单向数据流。
<script setup>
const {parent,root} = getCurrentInstance()
</script>
attrs
和 listeners
这两个属性本质上其实还是props和emit那一套,只不过我们不用在子组件里声明,可以直接使用,大部分用于组件库这种,有很多属性的情况,代码我就不再赘述了。
provide
和 inject
这里其实是Vue提供的一种依赖注入的方式,可以把值其他东西通过注入依赖的方式传递,有点类似于react
的context
。
- 父组件
<script setup>
import { countSymbol } from './injectionSymbols'
// 提供静态值
provide('path', '/project/')
// 提供响应式的值
const count = ref(0)
provide('count', count)
// 提供时将 Symbol 作为 key
provide(countSymbol, count)
</script>
- 子组件就通过
inject
获取值就行。
<script setup>
import { countSymbol } from './injectionSymbols'
const path = inject('path')
const count = inject('count')
// 当 Symbol 作为 key时特殊一点
const count2 = inject(countSymbol)
</script>
Vuex 或者 Pinia
这个没什么好说的,官方的状态管理库,但是要注意一下使用条件,我个人觉得如果你是大型应用,状态特别多,跨组件传值也特别多的时候再使用,否则确实有点累赘了。
EventBus
今天晚上的重头戏来了,这个问题也是我那次回答的亮点,我能从面试官眼中看到明显的惊讶,显然他没想到如此巧妙的方法。
一般实现
所谓的EventBus,其实就是就一个观察者模式,通过注册与通知来实现组件间的通讯,我随手写一下我自己的实现
class Emitter{
events = new Map()
on(name,cb){ //添加事件监听
if(this.events.has(name)){
this.events.get(name).add(cb)
}else{
const cbs = new Set([cb])
this.events.set(name,cbs)
}
}
off(name,cb){ //解除事件监听
if(this.events.has(name)){
const cbs = this.events.get(name)
cbs.delete(cb)
if(cbs.size === 0){
this.events.delete(name)
}
}
emit(name,data){ // 触发响应事件
if(this.events.has(name)){
const cbs = this.events.get(name)
cbs.forEach(cb=>cb(data))
}
}
clear(){ // 清除全部事件
this.events.clear()
}
}
这样就可以实现一个基本的EventBus,使用的时候需要把实例放到全局,如果你还想进一步优化,最好写一个单例,但是如果是面试你能这样说基本上就够了。 But。。。
我的答案
我给面试官回答,原生js都有,干嘛写,写起来还复杂了。我用的正是CustomEvent
,代码如下
// 触发事件
const evt = new CustomEvent('name', {detail:'事件携带的data'})
document.body.dispatchEvent(evt)
触发事件的时候注意在哪个具体的DOM上触发,我一边图方便在body上,你也可以在更具体的dom上触发。监听就是咱们常用的addEventListener
.
// 监听事件
document.body.addEventListener('name',({detail})=>{console.log(detail)})
这样就实现了上面那么长代码的功能,而且是js原生自带的,兼容性也是非常的棒,不存在任何问题。
当我这样回答完之后,邪魅一笑,面试官大惊失色,竟然说:还能这样!
完结