Vue组件初始化时序与异步资源加载的竞态问题实战解析

项目场景背景

最近在做基于 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,子组件就能正常调用监听器。但实际测试时发现:

  1. 点击事件的回调没触发(确定监听器逻辑没问题,2D 地图切换时能正常工作)。
  2. 断点调试发现 :子组件的 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) { /* 执行操作 */ }
})

技术要点总结

  1. 组件生命周期 ≠ 资源就绪 :Vue 的 onMounted 只保证 DOM 挂载完成,异步资源可能还没加载好。
  2. 防御式编程:在访问异步资源前加判断,避免空指针。
  3. 显式信号优于隐式依赖 :用 isMapReady 代替"猜"资源是否就绪。
  4. 调试技巧 :学会用debugger调试是多么的重要

最后的思考

这次问题让我深刻体会到,在异步世界里,永远不要假设资源已经就绪 。组件通信就像两个人传递接力棒,必须确保对方准备好再放手。虽然最终用 isMapReady 解决了问题,但过程中也学到了很多:比如如何用 Promise 链式等待、如何用事件总线解耦、甚至想试试 Pinia 状态管理,就当熟悉一下Pinia。

如果下次再遇到类似问题,我可能会更早想到"显式信号"这个思路,而不是一头扎进代码里找 Bug。这也让我深刻的感受到了异步没想的那么简单,还是经验太少了啊。希望这篇文章对你有所帮助。

相关推荐
成为大佬先秃头2 小时前
渐进式JavaScript框架:Vue 过渡 & 动画 & 可复用性 & 组合
开发语言·javascript·vue.js
JIngJaneIL2 小时前
基于java+ vue家庭理财管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
GISer_Jing2 小时前
Taro跨端开发实战:JX首页实现_Trae SOLO构建
前端·javascript·aigc·taro
vipbic2 小时前
基于 Nuxt 4 + Strapi 5 构建高性能 AI 导航站
前端·后端
不要em0啦3 小时前
从0开始学python:简单的练习题3
开发语言·前端·python
老华带你飞3 小时前
电商系统|基于java + vue电商系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
星月心城3 小时前
面试八股文-JavaScript(第四天)
开发语言·javascript·ecmascript
大猫会长3 小时前
关于http状态码4xx与5xx的背锅问题
前端
喝拿铁写前端3 小时前
AI 驱动前端开发覆盖的能力全景拆解
前端·javascript·人工智能