Vue 懒挂载组件设计
2026/7/7 6:20:37 网站建设 项目流程

Vue 懒挂载组件设计:LazyMountComponent核心逻辑解析

在长页面或复杂表单里,一次性渲染所有模块,容易带来首屏慢、滚动卡顿的问题。
LazyMountComponent的作用,就是把真实内容的渲染时机延后到“用户快看到它的时候”。

它的核心目标有 3 个:

  1. 默认先不渲染真实内容
  2. 进入可视区后再挂载
  3. 卸载时保留高度,避免页面抖动

一、组件做了什么

模板很简单:

<div:id="compId"ref="containerRef":style="containerStyle"><div v-if="isMounted"ref="contentRef"><slot></slot></div><div v-elseclass="placeholder"><el-skeleton:rows="5"animated/></div></div>

只有两种状态:
isMounted = true:显示真实内容
isMounted = false:显示骨架屏
所以整个组件的关键,就是 控制 isMounted 什么时候切换。

二、核心逻辑

  1. 用 IntersectionObserver 判断是否进入可视区
constinitObserver=()=>{observer=newIntersectionObserver((entries)=>{entries.forEach(handleIntersection);},{root:null,rootMargin:props.rootMargin,threshold:props.threshold,});observer.observe(containerRef.value);};

当组件容器进入视口时,触发挂载逻辑;离开视口时,触发卸载逻辑。
这是整个懒加载机制的入口。

2 . 进入可视区后,不立即挂载,而是延迟挂载

constscheduleMount=(entry:IntersectionObserverEntry)=>{clearMountTimer();mountTimer=setTimeout(()=>{if(!entry.isIntersecting)return;mountComponent();},MOUNT_DELAY);};

这里的设计重点是:
不是一进入视口就立刻渲染,而是延迟一段时间再挂载。
这样做的好处是,用户快速滚动时,不会把一闪而过的模块也渲染出来,能减少无效渲染。

3. 真正挂载时,只做必要操作

constmountComponent=()=>{isMounted.value=true;clearMountTimer();stopObservingWhenNeeded();};

这里最核心的是:
1.把 isMounted 改成 true
2.清理定时器
3.如果后续不需要卸载,就停止继续监听可视区
也就是说,这个组件既支持“懒加载一次”,也支持“可见时挂载,不可见时卸载”。

4. 用 ResizeObserver 缓存真实内容高度

constcacheContentHeight=()=>{constcurrentHeight=contentRef.value?.offsetHeight??0;if(currentHeight>0){containerMinHeight.value=currentHeight;}};

这一步非常关键。因为内容挂载后高度不一定固定,可能会因为表格、异步数据、折叠面板展开而变化。所以组件会持续记录真实内容高度,并把它保存到容器的 min-height。
目的只有一个:后续即使内容被卸载,页面高度也不要突然塌掉。

5. 离开可视区时,可按配置卸载内容

constunmountComponent=()=>{if(!props.unmountWhenInvisible||!isMounted.value)return;cacheContentHeight();unobserveContent();isMounted.value=false;};

这里的顺序不能乱:
先缓存高度
再取消监听
最后卸载内容
如果先卸载,再取高度,就拿不到真实尺寸了,页面容易抖动。

三、总结

这个组件可以概括成一句话:
用 IntersectionObserver 决定什么时候渲染,用 ResizeObserver 记录真实高度,用占位高度保证页面不抖。
如果页面里有很多重模块,比如表格、折叠区、复杂表单,这类组件很适合拿来做首屏和滚动性能优化。

四、技术亮点

  1. 设计并实现了高性能可视区懒加载组件,支持组件级按需渲染
  2. 创新引入 1500ms 延迟挂载机制,优化快速滚动场景下的渲染抖动
  3. 设计了动态高度缓存 + ResizeObserver 联动方案,实现占位高度自适应
  4. 支持 unmountWhenInvisible 可配置策略,在滚动离开视口后自动卸载 DOM 并回收内存
  5. 封装了 immediate 即时渲染 + triggerMount 手动控制的双模式接口

五、完整代码

<template><div:id="compId"ref="containerRef"class="lazy-container":style="containerStyle"><div v-if="isMounted"ref="contentRef"><slot></slot></div><div v-elseclass="placeholder"><el-skeleton:rows="5"animated/></div></div></template><script setup lang="ts">interfaceProps{threshold?:number;rootMargin?:string;placeholderHeight?:number;immediate?:boolean;unmountWhenInvisible?:boolean;compId:string;}constDEFAULT_PLACEHOLDER_HEIGHT=400;constMOUNT_DELAY=1500;constprops=withDefaults(defineProps<Props>(),{threshold:0.2,rootMargin:'0px',placeholderHeight:0,//占位符高度immediate:false,//是否立即挂载组件unmountWhenInvisible:false,//是否在不可见时卸载组件(当组件滚出可视区后再次卸载组件)compId:'',});constcontainerRef=ref<HTMLElement>();constcontentRef=ref<HTMLElement>();constisMounted=ref(props.immediate);constcontainerMinHeight=ref(props.placeholderHeight||DEFAULT_PLACEHOLDER_HEIGHT);constcontainerStyle=computed(()=>({minHeight:`${containerMinHeight.value}px`,}));letobserver:IntersectionObserver|null=null;letresizeObserver:ResizeObserver|null=null;letmountTimer:ReturnType<typeofsetTimeout>|null=null;// 清理延迟挂载定时器,避免重复触发或组件销毁后继续执行。constclearMountTimer=()=>{if(!mountTimer)return;clearTimeout(mountTimer);mountTimer=null;};// 记录当前内容高度,供骨架屏和卸载后的容器占位复用,避免页面抖动。constcacheContentHeight=()=>{constcurrentHeight=contentRef.value?.offsetHeight??0;if(currentHeight>0){containerMinHeight.value=currentHeight;}};// 当内容挂载后不再需要反复监听可视区时,停止交叉观察。conststopObservingWhenNeeded=()=>{if(props.unmountWhenInvisible)return;observer?.disconnect();};// 取消对真实内容高度的监听,通常在内容卸载或组件销毁时调用。constunobserveContent=()=>{if(!resizeObserver||!contentRef.value)return;resizeObserver.unobserve(contentRef.value);};// 在真实内容完成渲染后开始监听高度变化,并立即缓存一次当前高度。constobserveContent=async()=>{if(!resizeObserver||!isMounted.value)return;awaitnextTick();if(!contentRef.value)return;resizeObserver.observe(contentRef.value);cacheContentHeight();};// 初始化尺寸观察器,确保内容高度变化时同步更新容器占位高度。constinitResizeObserver=()=>{if(resizeObserver)return;resizeObserver=newResizeObserver(()=>{if(!isMounted.value)return;cacheContentHeight();});};// 执行真实内容挂载,并在必要时停止继续监听可视区变化。constmountComponent=()=>{isMounted.value=true;clearMountTimer();stopObservingWhenNeeded();};// 在允许“离开视口即卸载”时,先缓存高度再卸载真实内容。constunmountComponent=()=>{if(!props.unmountWhenInvisible||!isMounted.value)return;cacheContentHeight();unobserveContent();isMounted.value=false;};// 进入可视区后延迟挂载,减少快速滚动场景下的无效渲染。constscheduleMount=(entry:IntersectionObserverEntry)=>{clearMountTimer();mountTimer=setTimeout(()=>{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();observer=newIntersectionObserver((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>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询