项目场景背景
最近在做基于 Vue 3 + Cesium 的三维可视化监控系统时,遇到了一个让人抓耳挠腮的问题。父组件 ZhjcContent 负责初始化 Cesium 地图实例,通过 mapRef 将地图组件引用传递给子组件 RightIndex。子组件需要在地图初始化完成后注册点击监听器、渲染报警点位等操作。
html
<!-- 父组件模板 -->
<MapIndex ref="mapRef" />
<RightIndex :mapRef="mapRef" ref="rightIndexRef" />
遇到的问题
在子组件中写了 watch 去监听父组件传递的 mapRef,当 mapRef 有值时执行一些操作:
ts
// 子组件监听 mapRef
watch(
() => props.mapRef,
async (newVal) => {
if (newVal) {
// 渲染报警点位
renderAlertPoints()
// 开启监听点击地图事件器
newVal.zhjc_registerMapClickListener(showAlertPopup)
}
},
{ immediate: true }
)
按理说,父组件初始化完地图后传递 mapRef,子组件就能正常调用监听器。但实际测试时发现:
- 点击事件的回调没触发(确定监听器逻辑没问题,2D 地图切换时能正常工作)。
- 断点调试发现 :子组件的
watch回调居然先于父组件初始化地图逻辑执行,导致zhjc_registerMapClickListener中检测到地图实例未就绪,直接退出。
我的排查过程
当时真的很懵,反复检查了监听函数的实现,确认逻辑没问题。直到我开始打日志和断点,才发现问题所在:
- 子组件的
watch在父组件onMounted还没执行完时就触发了。 mapRef.value是组件引用(壳对象),但内部的 Cesium Viewer 实例还没初始化完成。- 误以为只要
mapRef有值,就能直接调用方法,其实getViewer()返回的是undefined。
解决方案
最终采用方案:父组件定义 isMapReady 通知子组件
我意识到问题的核心在于:组件引用存在 ≠ 资源就绪 。于是,在父组件中加了一个开关 isMapReady,用来告诉子组件"地图准备好了"。
父组件实现
ts
const isMapReady = ref(false)
onMounted(async () => {
await nextTick()
if (mapRef.value) {
await mapRef.value.initMap() // 等待异步初始化
mapObjRef.value = mapRef.value.getMapObj()
isMapReady.value = true // 开关打开
}
})
<RightIndex
:mapRef="mapRef"
:isMapReady="isMapReady"
/>
子组件监听
ts
const props = defineProps<{
mapRef: any;
isMapReady: boolean;
}>()
watch(
() => props.isMapReady,
(newVal) => {
if (newVal && props.mapRef?.value) {
renderAlertPoints()
props.mapRef.value.zhjc_registerMapClickListener(showAlertPopup)
}
},
{ immediate: true }
)
地图监听器加固
ts
zhjc_registerMapClickListener(callback) {
if (!this.dpzsMapObj?.viewer || this.dpzsMapObj.viewer.isDestroyed()) {
console.warn('地图未就绪或已销毁')
return
}
// 正常注册事件逻辑
}
后面我又想了几种其他可能的解决思路
1. 子组件暴露回调函数供父组件调用(子传父)
在子组件中把回调函数暴露出来,然后在父组件中进行获取子组件的实例去调用回调函数
子组件实现
ts
// 子组件 RightIndex.vue
<script setup lang="ts">
const props = defineProps<{
mapRef: any;
}>()
// 暴露给父组件的方法
defineExpose({
showAlertPopup
})
</script>
父组件调用
ts
// 父组件 ZhjcContent.vue
const rightIndexRef = ref(null)
onMounted(async () => {
await nextTick()
if (mapRef.value) {
await mapRef.value.initMap()
// 主动调用子组件的函数
if (rightIndexRef.value && rightIndexRef.value.showAlertPopup) {
mapRef.value.zhjc_registerMapClickListener(showAlertPopup)
}
}
})
2. Promise 链式等待
让父组件暴露一个 Promise,子组件主动等待初始化完成:
ts
// 父组件
const mapInitPromise = ref<Promise<void> | null>(null)
onMounted(async () => {
mapInitPromise.value = mapRef.value.initMap()
await mapInitPromise.value
isMapReady.value = true
})
// 子组件
watch(() => props.mapRef, async (newVal) => {
if (newVal && mapInitPromise.value) {
await mapInitPromise.value
// 执行操作
}
})
3. 自定义事件总线
用全局事件通知子组件:
ts
// 父组件
import { createEventBus } from 'vue'
const eventBus = createEventBus()
onMounted(async () => {
await mapRef.value.initMap()
eventBus.emit('map-ready')
})
// 子组件
onMounted(() => {
eventBus.on('map-ready', () => {
// 执行操作
})
})
4. Pinia 状态管理
用状态管理库解耦父子组件,说实话,这个地方用全局状态管理,实在是有点小题大作,虽然功能确实能实现,但是并不好,写出来知识让大家作为思路参考一下:
ts
// stores/mapStore.ts
export const useMapStore = defineStore({
state: () => ({
isReady: false
})
})
// 父组件
useMapStore().isReady = true
// 子组件
watch(() => useMapStore().isReady, (ready) => {
if (ready) { /* 执行操作 */ }
})
技术要点总结
- 组件生命周期 ≠ 资源就绪 :Vue 的
onMounted只保证 DOM 挂载完成,异步资源可能还没加载好。 - 防御式编程:在访问异步资源前加判断,避免空指针。
- 显式信号优于隐式依赖 :用
isMapReady代替"猜"资源是否就绪。 - 调试技巧 :学会用
debugger调试是多么的重要
最后的思考
这次问题让我深刻体会到,在异步世界里,永远不要假设资源已经就绪 。组件通信就像两个人传递接力棒,必须确保对方准备好再放手。虽然最终用 isMapReady 解决了问题,但过程中也学到了很多:比如如何用 Promise 链式等待、如何用事件总线解耦、甚至想试试 Pinia 状态管理,就当熟悉一下Pinia。
如果下次再遇到类似问题,我可能会更早想到"显式信号"这个思路,而不是一头扎进代码里找 Bug。这也让我深刻的感受到了异步没想的那么简单,还是经验太少了啊。希望这篇文章对你有所帮助。