Vue 懒挂载组件设计

📅 2026/7/7 6:20:43
Vue 懒挂载组件设计
Vue 懒挂载组件设计LazyMountComponent核心逻辑解析在长页面或复杂表单里一次性渲染所有模块容易带来首屏慢、滚动卡顿的问题。LazyMountComponent的作用就是把真实内容的渲染时机延后到“用户快看到它的时候”。它的核心目标有 3 个默认先不渲染真实内容进入可视区后再挂载卸载时保留高度避免页面抖动一、组件做了什么模板很简单div:idcompIdrefcontainerRef:stylecontainerStylediv v-ifisMountedrefcontentRefslot/slot/divdiv v-elseclassplaceholderel-skeleton:rows5animated//div/div只有两种状态isMounted true显示真实内容isMounted false显示骨架屏所以整个组件的关键就是 控制 isMounted 什么时候切换。二、核心逻辑用 IntersectionObserver 判断是否进入可视区constinitObserver(){observernewIntersectionObserver((entries){entries.forEach(handleIntersection);},{root:null,rootMargin:props.rootMargin,threshold:props.threshold,});observer.observe(containerRef.value);};当组件容器进入视口时触发挂载逻辑离开视口时触发卸载逻辑。这是整个懒加载机制的入口。2 . 进入可视区后不立即挂载而是延迟挂载constscheduleMount(entry:IntersectionObserverEntry){clearMountTimer();mountTimersetTimeout((){if(!entry.isIntersecting)return;mountComponent();},MOUNT_DELAY);};这里的设计重点是不是一进入视口就立刻渲染而是延迟一段时间再挂载。这样做的好处是用户快速滚动时不会把一闪而过的模块也渲染出来能减少无效渲染。3. 真正挂载时只做必要操作constmountComponent(){isMounted.valuetrue;clearMountTimer();stopObservingWhenNeeded();};这里最核心的是1.把 isMounted 改成 true2.清理定时器3.如果后续不需要卸载就停止继续监听可视区也就是说这个组件既支持“懒加载一次”也支持“可见时挂载不可见时卸载”。4. 用 ResizeObserver 缓存真实内容高度constcacheContentHeight(){constcurrentHeightcontentRef.value?.offsetHeight??0;if(currentHeight0){containerMinHeight.valuecurrentHeight;}};这一步非常关键。因为内容挂载后高度不一定固定可能会因为表格、异步数据、折叠面板展开而变化。所以组件会持续记录真实内容高度并把它保存到容器的 min-height。目的只有一个后续即使内容被卸载页面高度也不要突然塌掉。5. 离开可视区时可按配置卸载内容constunmountComponent(){if(!props.unmountWhenInvisible||!isMounted.value)return;cacheContentHeight();unobserveContent();isMounted.valuefalse;};这里的顺序不能乱先缓存高度再取消监听最后卸载内容如果先卸载再取高度就拿不到真实尺寸了页面容易抖动。三、总结这个组件可以概括成一句话用 IntersectionObserver 决定什么时候渲染用 ResizeObserver 记录真实高度用占位高度保证页面不抖。如果页面里有很多重模块比如表格、折叠区、复杂表单这类组件很适合拿来做首屏和滚动性能优化。四、技术亮点设计并实现了高性能可视区懒加载组件支持组件级按需渲染创新引入 1500ms 延迟挂载机制优化快速滚动场景下的渲染抖动设计了动态高度缓存 ResizeObserver 联动方案实现占位高度自适应支持 unmountWhenInvisible 可配置策略在滚动离开视口后自动卸载 DOM 并回收内存封装了 immediate 即时渲染 triggerMount 手动控制的双模式接口五、完整代码templatediv:idcompIdrefcontainerRefclasslazy-container:stylecontainerStylediv v-ifisMountedrefcontentRefslot/slot/divdiv v-elseclassplaceholderel-skeleton:rows5animated//div/div/templatescript setup langtsinterfaceProps{threshold?:number;rootMargin?:string;placeholderHeight?:number;immediate?:boolean;unmountWhenInvisible?:boolean;compId:string;}constDEFAULT_PLACEHOLDER_HEIGHT400;constMOUNT_DELAY1500;constpropswithDefaults(definePropsProps(),{threshold:0.2,rootMargin:0px,placeholderHeight:0,//占位符高度immediate:false,//是否立即挂载组件unmountWhenInvisible:false,//是否在不可见时卸载组件当组件滚出可视区后再次卸载组件compId:,});constcontainerRefrefHTMLElement();constcontentRefrefHTMLElement();constisMountedref(props.immediate);constcontainerMinHeightref(props.placeholderHeight||DEFAULT_PLACEHOLDER_HEIGHT);constcontainerStylecomputed(()({minHeight:${containerMinHeight.value}px,}));letobserver:IntersectionObserver|nullnull;letresizeObserver:ResizeObserver|nullnull;letmountTimer:ReturnTypetypeofsetTimeout|nullnull;// 清理延迟挂载定时器避免重复触发或组件销毁后继续执行。constclearMountTimer(){if(!mountTimer)return;clearTimeout(mountTimer);mountTimernull;};// 记录当前内容高度供骨架屏和卸载后的容器占位复用避免页面抖动。constcacheContentHeight(){constcurrentHeightcontentRef.value?.offsetHeight??0;if(currentHeight0){containerMinHeight.valuecurrentHeight;}};// 当内容挂载后不再需要反复监听可视区时停止交叉观察。conststopObservingWhenNeeded(){if(props.unmountWhenInvisible)return;observer?.disconnect();};// 取消对真实内容高度的监听通常在内容卸载或组件销毁时调用。constunobserveContent(){if(!resizeObserver||!contentRef.value)return;resizeObserver.unobserve(contentRef.value);};// 在真实内容完成渲染后开始监听高度变化并立即缓存一次当前高度。constobserveContentasync(){if(!resizeObserver||!isMounted.value)return;awaitnextTick();if(!contentRef.value)return;resizeObserver.observe(contentRef.value);cacheContentHeight();};// 初始化尺寸观察器确保内容高度变化时同步更新容器占位高度。constinitResizeObserver(){if(resizeObserver)return;resizeObservernewResizeObserver((){if(!isMounted.value)return;cacheContentHeight();});};// 执行真实内容挂载并在必要时停止继续监听可视区变化。constmountComponent(){isMounted.valuetrue;clearMountTimer();stopObservingWhenNeeded();};// 在允许“离开视口即卸载”时先缓存高度再卸载真实内容。constunmountComponent(){if(!props.unmountWhenInvisible||!isMounted.value)return;cacheContentHeight();unobserveContent();isMounted.valuefalse;};// 进入可视区后延迟挂载减少快速滚动场景下的无效渲染。constscheduleMount(entry:IntersectionObserverEntry){clearMountTimer();mountTimersetTimeout((){if(!entry.isIntersecting)return;mountComponent();},MOUNT_DELAY);};// 处理进入可视区事件已挂载则补充监听高度未挂载则安排延迟挂载。consthandleVisibleEntry(entry:IntersectionObserverEntry){if(isMounted.value){voidobserveContent();return;}scheduleMount(entry);};// 处理离开可视区事件取消待执行挂载并按配置决定是否卸载内容。consthandleHiddenEntry(){clearMountTimer();unmountComponent();};// 统一分发交叉观察结果根据可见状态执行挂载或卸载流程。consthandleIntersection(entry:IntersectionObserverEntry){if(entry.isIntersecting){handleVisibleEntry(entry);return;}handleHiddenEntry();};// 初始化交叉观察器用容器是否进入视口来驱动懒挂载逻辑。constinitObserver(){if(!containerRef.value)return;observer?.disconnect();observernewIntersectionObserver((entries){entries.forEach(handleIntersection);},{root:null,rootMargin:props.rootMargin,threshold:props.threshold,});observer.observe(containerRef.value);};// 根据挂载状态补充或移除内容高度监听保持占位高度准确。watch(isMounted,(mounted){if(mounted){voidobserveContent();return;}unobserveContent();});// 组件挂载后初始化观察器若要求立即渲染则直接挂载真实内容。onMounted((){initResizeObserver();initObserver();if(props.immediate){mountComponent();}});// 组件销毁前清理定时器和各类观察器避免残留引用。onUnmounted((){clearMountTimer();unobserveContent();observer?.disconnect();resizeObserver?.disconnect();});// 暴露给父组件的手动挂载入口用于跳过滚动检测直接显示内容。consttriggerMount(){mountComponent();};defineExpose({triggerMount,});/script