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

一、引入

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JavaScript设计模式之责任链模式

适用场景&#xff1a;一个完整的流程&#xff0c;中间分成多个环节&#xff0c;各个环节之间存在一定的顺序关系&#xff0c;同时中间的环节的个数不一定&#xff0c;可能添加环节&#xff0c;也可能减少环节&#xff0c;只要保证顺序关系就可以。 如下图&#xff1a; ES5写法…

flutter之bloc使用详解

flutter中一切皆为Widget&#xff0c;因此在我们开发中&#xff0c;往往业务和UI逻辑写在一起&#xff0c;这样不利于代码维护&#xff0c;因此状态管理框架久诞生了&#xff0c;这篇就开始讲一讲Bloc。 对于Bloc库有两个&#xff0c;如下图&#xff1a; flutter_bloc其实是对…

进程控制(三):进程替换

文章目录 进程控制&#xff08;三&#xff09;进程替换进程替换函数进程中的环境变量 总结 进程控制&#xff08;三&#xff09; 进程控制中的进程替换&#xff0c;下文我们学习进程替换的意义&#xff0c;以及进程替换的方式 进程替换 初步认识进程替换&#xff0c;我们先使…

Spring Cloud应用- Eureka原理、搭建

初期对Spring Cloud的学习以应用搭建为主&#xff0c;所以内容不会太枯燥。 一直以来&#xff0c;自以为Spring全家桶的学习中&#xff0c;Spring framework是基础中的基础&#xff0c;部分内容也还是必须要读源码去理解底层原理&#xff0c;SpringMVC、SpringBoot&#xff0c…

【ElasticSearch系列-03】ElasticSearch的高级句法查询Query DSL

ElasticSearch系列整体栏目 内容链接地址【一】ElasticSearch下载和安装https://zhenghuisheng.blog.csdn.net/article/details/129260827【二】ElasticSearch概念和基本操作https://blog.csdn.net/zhenghuishengq/article/details/134121631【二】ElasticSearch的高级查询Quer…

Linux上编译sqlite3库出现undefined reference to `sqlite3_column_table_name‘

作者&#xff1a;朱金灿 来源&#xff1a;clever101的专栏 为什么大多数人学不会人工智能编程&#xff1f;>>> 在Ubuntu 18上编译sqlite3库后在运行程序时出现undefined reference to sqlite3_column_table_name’的错误。网上的说法是说缺少SQLITE_ENABLE_COLUMN_M…

解决ModuleNotFoundError: No module named ‘yaml‘

报错&#xff1a;ModuleNotFoundError: No module named yaml 使用&#xff1a; pip install yaml 仍然报错&#xff1a; 最终解决方案&#xff1a; pip install pyyaml 或者 conda install pyyaml

百度竞价排名推广对比自然排名哪一个更具优势-华媒舍

在搜索引擎结论网页页面&#xff08;SERP&#xff09;中&#xff0c;我们经常会看到一些网站链接及其广告栏。这种连接一般分为两种类型&#xff1a;百度竞价推广排名推广与自然排名。究竟哪个更有优势&#xff1f;本文将对这几种排名形式进行科谱详细介绍。 什么叫百度竞价推广…

右击显示Pycharm打开教程

效果图 操作流程 win r 输入 regedit 回车打开注册表编辑器 2.找到 shell 路径 计算机\HKEY_CLASSES_ROOT\Directory\shell3.在 shell 下新建项&#xff0c;名称为 Pycharm 单击Pycharm文件夹&#xff0c;双击默认项&#xff0c;修改默认值&#xff0c;这个数值就是你右击后…

KaiwuDB 内核解析 - SQL 查询的生命周期

一、概述 KaiwuDB 内核解析系列共分上下两部分&#xff0c;本文是该系列的第一部分&#xff0c;主要涵盖了网络协议到 SQL 执行器&#xff0c;解释 KaiwuDB 如何执行 SQL 查询&#xff0c;包括系统各个组件的执行路径&#xff08;网络协议、SQL 会话管理、解析器、执行计划及优…

ucos_conf、ucos_src和ucos_port

目录 ucos_conf 文件夹ucos_src 文件夹ucos_port 文件夹 在 uC/OS-II 中&#xff0c;ucos_conf、ucos_src 和 ucos_port 是三个不同的文件夹&#xff0c;它们的作用和功能有所不同&#xff1a; ucos_conf 文件夹 ucos_conf 文件夹&#xff1a;ucos_conf 文件夹包含了 uC/OS-II…

CSGO游戏里的饰品是如何被炒作起来的?

csgo倒狗们是如何操盘csgo饰品市场的&#xff1f; CSGO游戏里的饰品是如何被炒作起来的&#xff1f; 随着近几年csgo玩家数量急剧上升&#xff0c;倒狗在市场中的比例也在上升&#xff0c;之前的csgo饰品市场以散户居多&#xff0c;价格波动不大&#xff0c;现在倒狗大量涌入&a…

Docker学习——①

文章目录 1、什么是虚拟化、容器化&#xff1f;2、为什么要虚拟化、容器化&#xff1f;3、虚拟化实现方式3.1 应用程序执行环境分层3.2 虚拟化常见类别3.3 常见虚拟化实现3.3.1 主机虚拟化(虚拟机)实现3.3.2 容器虚拟化实现3.3.3 空间隔离实战--基础知识3.3.4 PID 隔离3.3.5 Mo…

【优选算法系列】【专题九链表】第一节.链表常用技巧和操作总结(2. 两数相加)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、链表常用技巧和操作总结二、两数相加 2.1 题目描述 2.2 题目解析 2.2.1 算法原理 2.2.2 代码编写总结 前言 一、链表常…

uniapp自定义权限菜单,动态tabbar

已封装为组件&#xff0c;亲测4个菜单项目可以切换&#xff0c; 以下为示例&#xff0c;根据Storage 中 userType 的 值&#xff0c;判断权限菜单 <template><view class"tab-bar pb10"><view class"tabli" v-for"(tab, index) in ta…

会声会影2024对比2023变化以及功能对比

全新会声会影2024版本现已登场&#xff0c;小伙伴们相信已经急不可待地想知道2024版到底有哪些新功能。对比2023版本&#xff0c;会声会影2024版本有没有功能的增强&#xff1f;事不宜迟&#xff0c;现在就让我们一起来看看会声会影2024对比2023的变化&#xff0c;包括功能对比…

什么是Babel?它的主要作用是什么?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

编译正点原子LINUXB报错make: arm-linux-gnueabihf-gcc:命令未找到

编译正点原子LINUX报错make: arm-linux-gnueabihf-gcc&#xff1a;命令未找到 1.报错内容2.解决办法3./bin/sh: 1: lzop: not found4.编译成功 1.报错内容 make: arm-linux-gnueabihf-gcc&#xff1a;命令未找到CHK include/config/kernel.releaseCHK include/generat…

unity3d 开发笔记

unity 3d Unity是一个游戏引擎&#xff0c;包含渲染引擎&#xff0c;物理引擎&#xff0c;碰撞检测&#xff0c;音效&#xff0c;动画效果&#xff0c;场景管理等系统。它的开发效率高、脚本使用C#开发、简单易用、跨平台&#xff08;可以导出各个平台的程序&#xff09;&…

家庭用洗地机哪个最好?家用洗地机选购

家里日常打扫&#xff0c;维持地面的清洁&#xff0c;清洁干湿垃圾这时候必不可缺的就是洗地机了&#xff0c;由于近年来洗地机行业的热度高涨&#xff0c;涌现了很多洗地机品牌&#xff0c;这也让消费者在挑选的时候无从下手&#xff0c;今天笔者就给大家讲讲洗地机挑选需要主…