性能优化之懒加载 - 基于观察者模式和单例模式的实现

一、引入

        在前端性能优化中,关于图片/视频等内容的懒加载一直都是优化利器。当用户看到对应的视图模块时,才去请求加载对应的图像。 原理也很简单,通过浏览器提供的 IntersectionObserver - Web API 接口参考 | MDN (mozilla.org),观察“哪个元素和视口交叉”,从而进行懒加载。

        这个API具有很好的性能,因为它的监听是异步的,不会影响JS的主线程,所以比传统的“监听页面滚动”更佳。关于API的使用,这里就不做过多说明了,主要操作如下:

const DOM = document.querySelector('img')
const io = new IntersectionObserver((entries) => {entries.forEach((k) => {//回调函数,可以利用 k.target 是否和我们要监听的DOM元素相等,来判断当前是否是我们要监听的目标元素if(k.target === DOM){ /* 做懒加载的操作 */}});
}, {/*一些配置,详见MDN文档*/});
io.observe(DOM) //添加监听

 二、可优化的点

        值得注意的是,一个observer实例,可以监听多个DOM元素。如果我们需要封装一个图片组件,并实现它的懒加载,那么“每个组件都创建一个IntersectionObserver实例” 显然是不划算的,如果页面上有上百个图片,就会创建出上百个实例。

        针对这种情况,并且不想破坏组件的封装性,于是考虑把实例提升到全局,封装一个hook,从而每个组件都能自行添加入该实例的观察对象中。但是,监听的回调函数是创建实例的时候就决定的,后续添加进入的DOM元素,在回调函数中无法判断“是否轮到自己”了

三、观察者模式

        有什么办法能够让DOM元素动态的进入回调函数呢? 我们可以利用对象引用地址不变的特性,动态的往对象里添加数据,这样在回调函数触发时,就能够取出正确的数据了

        这里我的灵感其实来源于Vue3的响应式原理, 收集依赖 --> 监听 --> 触发依赖。(Vue3是多对多的发布-订阅模式, 这里是 一对多的观察者模式

/**回调函数的类型*/
type ObserverCallback = (entryData: IntersectionObserverEntry) => void
/** 键是DOM元素,值是该元素的回调函数Set (考虑到可能一个元素会有多个回调) */ 
const watchMap = new WeakMap<Element, Set<ObserverCallback>>()
const io = new IntersectionObserver((entries) => {entries.forEach((k) => {const set = watchMap.get(k.target)if(set){set.forEach((fn) => fn(k)) //从weakMap中取出对应的监听事件触发} });
}, {/*一些配置,详见MDN文档*/}); 

        剩下要做的就是“依赖收集”了。基于面向对象的思想 (可以创建多个实例,多处复用,互不干扰)。

        当有DOM元素需要被监听时,添加进weakMap中;需要取消监听时,移除; observer触发回调时,取出对应的元素的依赖,执行回调函数

        手写过观察者模式或者发布订阅模式的小伙伴,应该对下面的代码构造很熟悉。

/**视口监听器 - 观察者模式 */
export class ViewportObserverWatcher {/**IntersectionObserver 实例 */io: IntersectionObserver/**当前正在监听的元素的weakMap */watchMap = new WeakMap<Element, Set<ObserverCallback>>()constructor(options?: IntersectionObserverInit) {this.io = new IntersectionObserver((entries) => {entries.forEach((k) => {this.watchMap.get(k.target)?.forEach((fn) => fn(k)) //从weakMap中取出对应的监听事件触发});}, options);}/**添加对元素的一个监听回调,可以选择触发条件* @param target 目标元素* @param callback 回调函数* @param condition 触发回调条件 `true | false | undefined` 分别对应 `与视口边界交叉 | 不与视口交叉 | 都`*/addWatch = (target: Element, callback: ObserverCallback, condition?: boolean) => {const _callback: ObserverCallback = (k) => {if (condition == undefined) { }//无论如何都触发 else if ((condition !== k.isIntersecting)) return //当触发条件和实际情况不相同时,不触发 callback(k)}if (this.watchMap.has(target)) {this.watchMap.get(target)!.add(_callback)} else {this.io.observe(target)this.watchMap.set(target, new Set([_callback]))}}/**取消对元素的某个回调 */removeWatch = (target: Element, callback: ObserverCallback) => {const set = this.watchMap.get(target)if (set) {set.delete(callback)if (set.size === 0) {this.watchMap.delete(target)this.io.unobserve(target)}}}/**取消对该元素的全部回调 */cancelWatch = (target: Element) => {this.watchMap.delete(target)this.io.unobserve(target)}
}

四、写个Hook吧

1. 元素创建时,加入io的监听;

2. 触发懒加载之后,取消对该元素的监听。

3. 依赖项变化后,重复前面的逻辑。
4. 只要是元素,都能进行监听,不只是图片/视频。有需要使用到该功能的元素都能使用。

import { DependencyList, RefObject, useEffect, useRef } from "react";/**视口监听器 - 单例模式 */
const viewportObserver = new ViewportObserverWatcher() //注:如果你是NextJs, 在NextJS build的时候,不能直接实例化IntersectionObserver,否则会报错 (因为在走服务端代码) 可以先设置为null,后续给这个变量赋值/**懒加载Hook。懒加载触发后,将会取消监听* @param watchRef 要监听的DOM元素* @param onEntering 元素进入视口的回调函数* @param onDestroy useEffect的return中要做的事* @param deps useEffect的依赖数组 (当什么变化时,需要重新开始懒加载流程)*/
const useLazyLoad = (watchRef: RefObject<HTMLElement>, onEntering: ObserverCallback, onDestroy?: () => void, deps: DependencyList = []) => {/**是否完成懒加载 */const isLazySuccess = useRef(false);useEffect(() => {if (!watchRef.current) return; const callback: ObserverCallback = (k) => {//因为只要和视口在交叉,就会不断触发这个函数,故需要使用一个标识符来限制 if (isLazySuccess.current === false) {onEntering(k)isLazySuccess.current = true;viewportObserver!.removeWatch(watchRef.current!, callback) //加载完成就取消监听onEntering(k)}}viewportObserver.addWatch(watchRef.current, callback, true)return () => {if (watchRef.current && viewportObserver) viewportObserver.removeWatch(watchRef.current, callback); //卸载时也要取消监听 isLazySuccess.current = false;onDestroy && onDestroy()};}, deps)
}

使用方法: 核心思想:到了视口才赋值真实路径,其它时候使用占位符

/**视频组件 */
export default function Video({ src, className, otherProps }: VideoProps) {const outRef = useRef<HTMLDivElement>(null); //被监听的元素const [realSrc, setRealSrc] = useState<string>(); //存放展示的src,如果还没到视口就不展示useLazyLoad(outRef, () => setRealSrc(src));return (<div className={cn(className, "rounded")} ref={outRef}>{/* 其它逻辑.... */}{/* 正常展示视频 */}{realSrc && <video src={realSrc} {...otherProps} />}{/* 其它逻辑.... */}</div>);
}

五、使用效果

        结合前面文章写的的瀑布流组件,实现以下效果:

        (图片链接来源于 岁月小筑随机图片API接口-随机背景图片-随机图片API (xjh.me))

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/132535.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入理解强化学习——多臂赌博机:10臂测试平台

分类目录&#xff1a;《深入理解强化学习》总目录 为了大致评估贪心方法和 ϵ − \epsilon- ϵ−贪心方法相对的有效性&#xff0c;我们将它们在一系列测试问题上进行了定量比较。这组问题是2000个随机生成的 k k k臂赌博机问题&#xff0c;且 k 10 k10 k10。在每一个赌博机问…

【Head First 设计模式】-- 观察者模式

背景 客户有一个WeatherData对象&#xff0c;负责追踪温度、湿度和气压等数据。现在客户给我们提了个需求&#xff0c;让我们利用WeatherData对象取得数据&#xff0c;并更新三个布告板&#xff1a;目前状况、气象统计和天气预报。 WeatherData对象提供了4个接口&#xff1a; …

从零入门Chrome插件开发

什么是 Chrome 插件 谷歌浏览器在推出时就以其快速、安全和简洁的特点受到了广大用户的欢迎。随着浏览器的不断发展&#xff0c;谷歌为用户提供了插件开发平台&#xff0c;使开发者能够为浏览器添加各种功能和定制化选项。从此&#xff0c;插件成为了提升用户体验和个性化的重…

AI:54-基于深度学习的树木种类识别

🚀 本文选自专栏:AI领域专栏 从基础到实践,深入了解算法、案例和最新趋势。无论你是初学者还是经验丰富的数据科学家,通过案例和项目实践,掌握核心概念和实用技能。每篇案例都包含代码实例,详细讲解供大家学习。 📌📌📌在这个漫长的过程,中途遇到了不少问题,但是…

318. 最大单词长度乘积

318. 最大单词长度乘积 难度: 中等 来源: 每日一题 2023.11.06 给你一个字符串数组 words &#xff0c;找出并返回 length(words[i]) * length(words[j]) 的最大值&#xff0c;并且这两个单词不含有公共字母。如果不存在这样的两个单词&#xff0c;返回 0 。 示例 1&…

Canvas 梦幻树生长动画

canvas可以制作出非常炫酷的动画&#xff0c;以下是一个梦幻树的示例。 效果图 源代码 <!DOCTYPE> <html> <head> <meta http-equiv"Content-Type" content"text/html; charsetutf-8" /> <title>梦幻数生长动画</title&…

【数据结构】树与二叉树(二):树的表示C语言:树形表示法、嵌套集合表示法、嵌套括号表示法 、凹入表示法

文章目录 5.1 树的基本概念5.1.1 树的定义5.1.2 森林的定义5.1.3 树的术语5.1.4 树的表示1&#xff0e;树形表示法2&#xff0e;嵌套集合表示法结构体创建树主函数 3&#xff0e;嵌套括号表示法结构体创建树嵌套括号表示法主函数 4&#xff0e;凹入表示法结构体创建树凹入表示法…

O(1)时间复杂度通过位运算来判断两字符串是否有公共字符方法

判断两个字符串是否有公共字符暴力做法需要O(n^2)&#xff0c;而通过位运算优化&#xff0c;可以节省不少时间复杂度。 以小写26字母为例&#xff0c;判断两个字符串是否存在公共字符。 使用位运算&#xff0c;创建一个长度为2的数组&#xff0c;每个位置的长度为26&#xff…

ABC 327

比赛传送门 引 A,B,C题太水了不想写了&#xff0c;G太难了不会 所以记录一下D,E,F 正文 ——————————————————————————————————— D. Good Tuple Problem 题目传送门 解法1 建图跑 D f s Dfs Dfs ,判断是否为二分图 时间复杂度 O (…

C#开发的OpenRA游戏之生命值

caimouse写于深圳 2023.11.6 C#开发的OpenRA游戏之生命值 前面已经分析了步兵攻击兵营的情况,通过子弹类不断射向兵营,就会导致兵营的损伤,这个损伤表现为生命值。定义如下: Health: HP: 60000 根据OpenRA的设计原则,每一个属性,就会生成一个Info信息类,再创建一个定…

python调用飞书机器人发送文件

当前飞书webhook机器人还不支持发送文件类型的群消息&#xff0c;可以申请创建一个机器人应用来实现群发送文件消息。 创建机器人后&#xff0c;需要开通一系列权限&#xff0c;然后发布。由管理员审核通过后&#xff0c;才可使用。 包括如下的权限&#xff0c;可以获取群的c…

kprobe 内核实现原理

kprobe是linux内核的一个重要的特性&#xff0c;是其他内核调试工具(perf&#xff0c;systemtap)的基础设施&#xff0c;同时内核BPF也是依赖于kprobe。 Kprobe结构体 < include/linux/kprobe.h > struct kprobe {struct hlist_node hlist; /* 所有注册的kprob…

深度学习服务器(Linux)开发环境搭建教程

当你拿到一台服务器的使用权时&#xff0c;最头疼的莫过于登陆服务区并配置开发环境。本文将从0开始&#xff0c;讲述一台刚申请的服务器远程登陆并配置开发环境的全过程。希望对你有所帮助 1.登陆服务器 打开MobaXterm软件&#xff0c;创建一个新的Session&#xff0c;选择S…

Linux 编译链接那些事儿(02)C++链接库std::__cxx11::basic_string和std::__1::basic_string链接问题总结

1 问题背景说明 在自己的项目源码中引用libeasysqlite.so时编译成功&#xff0c;但运行时遇到问题直接报错&#xff0c;找不到符号 symbol&#xff1a;_ZN3sql5FieldC1ENSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEENS_10field_typeEi。 2 问题描述和解…

图及谱聚类商圈聚类中的应用

背景 在O2O业务场景中&#xff0c;有商圈的概念&#xff0c;商圈是业务运营的单元&#xff0c;有对应的商户BD负责人以及配送运力负责任。这些商圈通常是一定地理围栏构成的区域&#xff0c;区域内包括商户和用户&#xff0c;商圈和商圈之间就通常以道路、河流等围栏进行分隔。…

MySQL EXPLAIN查看执行计划

MySQL 执⾏计划是 MySQL 查询优化器分析 SQL 查询时⽣成的⼀份详细计划&#xff0c;包括表如何连 接、是否⾛索引、表扫描⾏数等。通过这份执⾏计划&#xff0c;我们可以分析这条 SQL 查询中存在的 问题&#xff08;如是否出现全表扫描&#xff09;&#xff0c;从⽽进⾏针对优化…

双十一运动健身好物推荐,这几款健身好物一定不要错过!

双十一购物狂欢节又要到了&#xff0c;又要到买买买的时候了&#xff01;相信有很多想健身的小白还在发愁不知道买啥装备&#xff1f;别急&#xff0c;三年健身达人这就给你们分享我的年度健身好物&#xff01; 第一款&#xff1a;南卡Runner Pro4s骨传导耳机 推荐理由&#…

新概念汽车3d720度全景vr制作尽可能还原汽车的真实细节

一、什么是VR全景看车 VR全景看车是通过虚拟现实技术实现逼真的汽车观赏和试乘体验。消费者可以通过智能手机或者电脑pc端&#xff0c;进入写实的虚拟现实的汽车展厅或者场景&#xff0c;进行车辆的透彻了解和体验。这种技术让消费者能够更加方便地了解汽车的外观、内饰和功能特…

独立IP主机怎么样?对网站有什么影响

对于现在企业来说&#xff0c;搭建网站是必不可少的&#xff0c;而大部分企业网站都会选择使用虚拟主机搭建&#xff0c;且使用的也是共享IP的这样会 有许多的弊端&#xff0c;所以部分站长会选择独立IP搭建。那么到底独立IP主机怎么样呢&#xff1f;使用独立IP主机搭建对网站有…

VSCode 连接不上 debian 的问题

之前一台笔记本上安装了 debian12&#xff0c;当时用 vscode 是可以连接上的&#xff0c;但今天连接突然就失败了&#xff0c;失败信息是这样的&#xff1a; 查看失败信息 因为 debian 是自动获取 ip 地址的&#xff0c;以前能连接上时&#xff0c;ip 地址是 104&#xff0c;然…