当前位置: 首页 > news >正文

Vue 懒挂载组件设计

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>
http://www.jsqmd.com/news/1139552/

相关文章:

  • DApp合约审计多少钱?项目有必要做智能合约审计吗?
  • Claude Code 中转站怎么选?从稳定性、缓存到成本的完整判断
  • CTF实战:从ZIP伪加密识别到时间戳掩码爆破的完整解题思路
  • 英雄联盟智能工具箱:3分钟掌握League Akari的终极使用技巧
  • 【计算机Java毕业设计案例】基于 SpringBoot 的农产品年度研究报告台账系统的设计与实现 基于 SpringBoot+Vue 的农产品数据研究成果管理系统(程序+文档+讲解+定制)
  • 2026国产CAD软件排行
  • 解放双手的智能游戏助手:基于图像识别的鸣潮自动化系统深度解析
  • 通用轴承密封选型参考梳理J型四氟油封密封圈是否合适?
  • 天幕传媒全品类活动物料定制,自主供应链与标准化品控体系
  • 猫抓浏览器扩展:新手快速上手指南 - 简单三步学会网页资源嗅探
  • Balena Etcher:如何重新定义系统镜像烧录的体验?
  • 2026年AI PPT制作工具横评:PaperRed、ChatGPT、豆包,谁才是答辩神器?
  • 直驱电机之DD马达选型
  • 128.工业级故障安全!PLC 电机正反转|软硬件双互锁 + 过载保护全套源码
  • Java毕设项目:基于 SpringBoot 的全国农产品调研数据管理系统的设计与实现 基于前后端分离的农业行业报告发布管理系统 (源码+文档,讲解、调试运行,定制等)
  • 敏捷测试转型:自动化与AI融合的工程实践与策略指南
  • Ubuntu 22.04 安装 MATLAB R2025a 全流程避坑指南
  • GyroFlow视频防抖终极指南:从新手到专家的完整教程
  • CNN网络层功能可视化:从CIFAR-10图像看卷积、池化层如何提取特征
  • 许可证遗失声明怎么写?从登报到补办,看这篇就够了
  • 终极视频号下载神器:3分钟完成全网资源批量采集
  • 天幕传媒|本地化综合传媒服务企业,全流程标准化服务体系介绍
  • 从 SQL Server 到平凯数据库:一家医疗 LIS 系统厂商的数据库迁移实践
  • 5分钟上手英雄联盟智能助手:Seraphine战绩查询与BP辅助终极指南
  • 网约房/民宿合规升级:基于IoT智能锁的身份核验与远程授权解决方案
  • 3.1 数据治理思想的演进:从数据库管理到数据要素战略的四十年来路
  • 游戏开发公司必须关注的十大网络安全问题与防护策略
  • 3D CNN 实战:R(2+1)D 与 SlowFast 在 PyTorch 中的实现与 Kinetics 400 验证集精度复现
  • Cursor不是VS Code替代品,而是AI原生IDE范式升级
  • Claude Code 用了大半年才悟出来的 6 个技巧,第 3 个直接省一半 Token