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

news2024/12/26 23:03:00

一、引入

        在前端性能优化中,关于图片/视频等内容的懒加载一直都是优化利器。当用户看到对应的视图模块时,才去请求加载对应的图像。 原理也很简单,通过浏览器提供的 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.coloradmin.cn/o/1168033.html

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

相关文章

Express框架开发接口之书城商店原型图和数据库设计

1.原型图 这是利用Axure画的&#xff0c;简单画一下原型图&#xff0c;根据他们的业务逻辑我们完成书城商店API开发 首页 分类 商品详情 购物车 个人中心 2.数据库设计 首页 首页导航 导航栏产品 通过点击导航栏对应id和产品id关联获取产品内容 设置外建 sql语句 CR…

CRM系统数据库是如何影响客户体验的?

CRM客户关系管理由概念到软件实体&#xff0c;已经有几十年的时间&#xff0c;随着信息技术的进步&#xff0c;数字化让CRM软件乘上快车&#xff0c;迅速成为各类企业的数字化管理工具。CRM客户管理系统的一个重要功能便是改善并提升客户体验&#xff0c;且CRM数据库是与客户体…

朝夕光年游戏高光全套解决方案

作为全球用户与开发者的游戏研发与发行业务品牌——朝夕光年&#xff0c;一直致力于服务全球玩家&#xff0c;帮助玩家在令人惊叹的虚拟世界中一起玩耍与创造。 游戏研发的过程中遇到很多玩家痛点和提效、拉新的问题&#xff0c;如何帮助玩家更好地记忆、记录、分享和传播自己的…

【Redis】String字符串类型-内部编码使用场景

文章目录 内部编码使用场景缓存功能计数功能共享会话手机验证码 内部编码 字符串类型的内部编码有3种&#xff1a; int&#xff1a;8个字节&#xff08;64位&#xff09;的⻓整型&#xff0c;存储整数embstr&#xff1a;压缩字符串&#xff0c;适用于表示较短的字符串raw&…

工业5G路由器;小体积 千兆高速通信组网

计讯物联工业路由器TR232&#xff0c;5G高速网络&#xff0c;超低时延、高可靠性&#xff0c;小体积、易安装、强兼容&#xff0c;串口/网口多设备接入联网&#xff0c;为用户提供高速稳定的数据传输通道 。    小体积5G工业路由器TR323&#xff0c;外形1047824mm&#xff0…

el-popover触发元素位置改变后更新弹出框的偏移位置

el-popover的使用如下&#xff1a;包含一个触发元素和一个弹出框元素 但是如果触发元素位置发生变化时&#xff0c;如根据弹框选择内容&#xff0c;会显示或隐藏对应的元素&#xff0c;从而导致弹出框触发元素的位置的变化&#xff0c;此时触发元素位置变化了&#xff0c;但是…

C++ 初阶 类和对象(中)

前言&#xff1a;C初阶系列&#xff0c;每一期博主都会使用简单朴素的语言将对应的知识分享给大家&#xff0c;争取让所有人都可以听懂&#xff0c;C初阶系列会持续更新&#xff0c;上学期间将不定时更新&#xff0c;但总会更的 目录 一、构造函数 1.1为什么要有构造函数&…

树状数组 and 线段树

目录 &#x1f996;解释 -- 树状数组 &#xff08;一&#xff09;公式 &#xff08;二&#xff09;操作 (1) 求前缀和 (2) 某个位置上的数更新 &#x1f996;解释 -- 线段树 &#x1f33c;1264. 动态求连续区间和 AC 树状 AC 线段树 &#x1f33c;1265. 数星星 暴…

数字化时代,数据仓库是什么?有什么用?

在激烈的市场竞争和全新的数字经济共同作用下&#xff0c;数字化转型成为了大多数企业的共识&#xff0c;也是获取数字经济的最佳方式。在整个数据价值生产链路中&#xff0c;数据仓库的主要作用就是中心化分发&#xff0c;将原始数据与数据价值挖掘活动隔离。 所有的原始数据…

大数据毕业设计选题推荐-自媒体舆情分析平台-Hadoop-Spark-Hive

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

Docker学习路线3:安装设置

Docker提供了一个名为Docker Desktop的桌面应用程序&#xff0c;简化了安装和设置过程。还有另一个选项可以使用Docker引擎进行安装。 Docker Desktop网站Docker引擎 Docker Desktop Docker Desktop是一款易于安装的应用程序&#xff0c;可使开发人员快速在其台式机上设置Do…

IntelliJ IDEA Services工具栏运行不显示端口问题解决

问题 如Spring Boot服务启动时&#xff0c;端口不显示。 解决 1、 清理所有缓存 2、 关闭IntelliJ IDEA后&#xff0c;到C:\Users\&#xff08;你自己的用户名&#xff09;\AppData\Local\Temp路径把所有文件都删除&#xff0c;因为时一个缓存&#xff0c;不影响其他软件…

世微 DC-DC平均电流双路降压恒流驱动器 LED车灯AP2813

产品描述 AP2813 是一款双路降压恒流驱动器,高效率、外 围简单、内置功率管&#xff0c;适用于 5-80V 输入的高精度降 压 LED 恒流驱动芯片。内置功率管输出最大功率可达 12W&#xff0c;最大电流 1.2A。 AP2813 一路直亮&#xff0c;另外一路通过 MODE1 切换 全亮&#xff0c…

推荐PHP付费进群源码

PHP付费进群源码带自动定位基于ThinkPHP框架开发的&#xff0c;可以快速搭建知识付费粉丝进群。 更新&#xff1a; 1.首页付款轮播 2.城市定位功能 3.更新及优化域名库及支付设置 4.新增一张图模板设置模式&#xff0c;简化后台模板设置 5.前后台其他优化 演示地址&#xff1a…

【Linux进程】环境变量

目录 Linux环境变量 基本概念 常见环境变量 查看环境变量方法 测试PATH 测试HOME 和环境变量相关的命令 环境变量的组织方式 通过代码如何获取环境变量 命令行参数 命令行第三个参数 通过第三方变量environ获取 通过系统调用获取或设置环境变量 ​编辑环境变量通…

【ML】分类问题

分类问题 classification&#xff1a;根据已知样本特征&#xff0c;判断输入样本属于哪种已知样本类。 常用入门案例&#xff1a;垃圾邮件检测、图像分类、手写数字识别、考试通过预测。 分类问题和回归问题的明显区别&#xff1a; 分类问题的结果是非连续型标签&#xff0c…

绝地求生msvcp140.dll丢失报错怎么办,这四个方法都可以解决

在回答这个问题之前&#xff0c;我们先来了解一下什么是msvcp140.dll。msvcp140.dll是微软Visual C 2015 Redistributable的一个组件&#xff0c;它包含了许多运行库文件&#xff0c;用于支持各种应用程序的正常运行。当你在玩《绝地求生》&#xff08;俗称“吃鸡”&#xff09…

网工内推 | 知名港企,需精通粤语,8-10年工作经验,最高60k

01 深圳市网新新思软件有限公司 招聘岗位&#xff1a;网络工程师 职责描述&#xff1a; 1、制定和执行网络设计、搭建、配置、优化、调整的实施方案&#xff1b; 2、负责网络运维保障工作&#xff0c;包括网络设备的定期巡检、一般网络异常处理、网络调试等日常运行及技术支持…

RHCSA --- 第二天

一、查看IP地址 [rootlocalhost ~] ip ad 对应四张网卡 第一张&#xff1a;环回网卡&#xff08;用于测试&#xff09; 第二张&#xff08;主要&#xff09;&#xff1a;以太网网卡&#xff08;ens160&#xff09; 2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP>…

【教3妹学编程-算法题】117. 填充每个节点的下一个右侧节点指针 II

2哥 : 3妹&#xff0c;听说你昨天去面试了&#xff0c;怎么样啊&#xff1f; 3妹&#xff1a;嗨&#xff0c;别提了&#xff0c;让我回去等通知&#xff0c;估计是没有通知了&#xff0c; 还浪费我请了一天假。 2哥 : 你又请假了啊&#xff0c; 你是怎么跟你那个严厉的老板请假…