Vue.js动态组件重复渲染,自身作为子组件嵌套
问题分析
你当前的Vue组件代理通过动态组件结合vue-lazy-hydration的hydrateWhenVisible实现懒加载+可见性水化,但出现了目标组件被渲染两次、自身嵌套,且偶尔报错的问题。从DevTools的表现来看,核心问题大概率和hydrateWhenVisible的包装逻辑、动态组件的渲染触发机制有关。
可能原因
hydrateWhenVisible的包装特性:这个方法会创建一个中间包装组件,而非直接返回目标组件。当动态组件渲染时,实际会先渲染这个包装组件,再由它渲染目标组件,看起来像是嵌套;同时如果包装组件的逻辑触发了两次目标组件实例化,就会出现重复渲染的情况。- computed属性的不必要触发:
getInstanceName作为computed属性,若data.instance_type或data本身发生频繁变化(哪怕是引用变化但值未变),会导致动态组件的:is值反复更新,触发组件的销毁和重建,表现为重复渲染。 - webpack懒加载的重复打包/加载:webpack的
[request]占位符可能在某些场景下导致同一组件被打包多次,或者懒加载的回调被重复执行,引发组件重复实例化。
解决方案
方案1:调整hydrateWhenVisible的使用方式
如果你的场景是客户端渲染而非SSR,可能不需要懒水化,直接使用Vue的懒加载即可。如果必须保留懒水化,尝试将包装逻辑移到组件内部,而非全局注册时包装:
<template> <component v-if="instanceComponent" :is="instanceComponent" :data="data" /> </template> <script> import WidgetTypes from './WidgetTypes' import { hydrateWhenVisible } from 'vue-lazy-hydration' export default { name: 'WidgetComponentProxy', props: { data: { type: Object, required: true } }, computed: { instanceName() { return WidgetTypes.getInstanceName(this.data.instance_type) }, instanceComponent() { if (!this.instanceName) return null return hydrateWhenVisible( () => import(/* webpackChunkName: "widget-[request]" */ `./dynamic/${this.instanceName}/${this.instanceName}.vue`), { observerOptions: { rootMargin: '100px' } } ) } } } </script>
方案2:缓存computed结果,避免不必要的组件重建
给组件名的计算逻辑添加缓存,确保只有当instance_type真正变化时才更新组件引用:
export default { // ...其他配置 data() { return { _instanceName: null } }, watch: { 'data.instance_type': { immediate: true, handler(newVal) { this._instanceName = WidgetTypes.getInstanceName(newVal) } } } }
模板中改为:is="_instanceName"
方案3:移除hydrateWhenVisible,手动实现可见性控制
如果懒水化不是必须的,直接用懒加载+自定义IntersectionObserver控制组件渲染:
<template> <div ref="widgetContainer"> <component v-if="isVisible && _instanceName" :is="_instanceName" :data="data" /> </div> </template> <script> import WidgetTypes from './WidgetTypes' const components = Object.keys(WidgetTypes.types).reduce((components, typeKey) => { const type = WidgetTypes.types[typeKey] components[type] = () => import(/* webpackChunkName: "widget-[request]" */ `./dynamic/${type}/${type}.vue`) return components }, {}) export default { name: 'WidgetComponentProxy', components: components, props: { data: { type: Object, required: true } }, data() { return { isVisible: false, _instanceName: null, observer: null } }, watch: { 'data.instance_type': { immediate: true, handler(newVal) { this._instanceName = WidgetTypes.getInstanceName(newVal) } } }, mounted() { this.observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { this.isVisible = true this.observer.unobserve(this.$refs.widgetContainer) } }, { rootMargin: '100px' }) this.observer.observe(this.$refs.widgetContainer) }, beforeDestroy() { this.observer?.disconnect() } } </script>
方案4:检查WidgetTypes的逻辑
确认WidgetTypes.getInstanceName是否存在返回重复值、或者同一instance_type对应多个组件名的情况,同时检查WidgetTypes.types的键值是否唯一,避免组件重复注册。
内容的提问来源于stack exchange,提问作者Lervad
相关产品推荐
相关产品推荐

