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

这篇具有很好参考价值的文章主要介绍了性能优化之懒加载 - 基于观察者模式和单例模式的实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、引入

        在前端性能优化中,关于图片/视频等内容的懒加载一直都是优化利器。当用户看到对应的视图模块时,才去请求加载对应的图像。 原理也很简单,通过浏览器提供的 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元素,在回调函数中无法判断“是否轮到自己”了

性能优化之懒加载 - 基于观察者模式和单例模式的实现,JavaScript,React.JS,观察者模式,react.js,前端,懒加载,typescript

三、观察者模式

        有什么办法能够让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))性能优化之懒加载 - 基于观察者模式和单例模式的实现,JavaScript,React.JS,观察者模式,react.js,前端,懒加载,typescript文章来源地址https://www.toymoban.com/news/detail-742891.html

到了这里,关于性能优化之懒加载 - 基于观察者模式和单例模式的实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 设计模式---观察者模式

    1,概念         属于行为模式的一种,定义了一种一对多的依赖关系,让多个观察者对象同时监听某一对象主题对象,这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。 在观察者模式中有如下角色: Subject:抽象主题(抽象被观察者),

    2024年02月15日
    浏览(50)
  • 设计模式:观察者模式

    定义 观察者模式(Observer Pattern)是一种行为设计模式,允许一个对象(称为“主题”或“可观察对象”)维护一组依赖于它的对象(称为“观察者”),当主题的状态发生变化时,会自动通知所有观察者对象。 应用场景 观察者模式适用于以下场景: 联动反应 :当一个对象

    2024年04月08日
    浏览(44)
  • 【设计模式】观察者模式

    观察者模式(又被称为发布-订阅(Publish/Subscribe)模式,属于行为型模式的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。 Subject:抽象主题(被观察者

    2024年02月13日
    浏览(37)
  • 设计模式——观察者模式

    观察者模式可以分为观察者和被观察者,观察者通过注册到一个被观察者中,也可视为订阅,当被观察者的数据发生改变时,会通知到观察者,观察者可以据此做出反应。 可以类比订阅报纸,报社就是被观察者,订阅者就是观察者,订阅者通过订阅报纸与报社建立联系,而报

    2024年02月15日
    浏览(38)
  • 行为型模式 | 观察者模式

    观察者模式又叫做发布-订阅(Publish/Subscribe)模式,定义了一种一对多的依赖关系 。让多个观察者对象同时监听某一个主题对象,这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。 软件系统常常要求在某一个对象的状态发生变化的时候

    2024年01月22日
    浏览(66)
  • 设计模式-观察者模式

    观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者

    2024年02月15日
    浏览(43)
  • 观察者模式和发布订阅模式

    观察者模式与发布订阅模式的区别: 1、观察者模式中只有观察者和被观察者,发布订阅模式中有发布者、订阅者、调度中心 2、观察者模式是被观察者发生变化时自己通知观察者,发布订阅模式是通过调度中心来进行分布订阅操作 vue2中响应式数据就是由Object.defineProperty()和

    2024年02月13日
    浏览(32)
  • 设计模式——14. 观察者模式

    观察者模式(Observer Pattern)是一种行为型设计模式,用于定义对象之间的一对多依赖关系,使得当一个对象的状态发生改变时,所有依赖于它的对象都能够自动收到通知并更新自己的状态,以保持与被观察对象的同步。观察者模式也被称为发布-订阅模式。 观察者模式包含以

    2024年02月07日
    浏览(35)
  • 设计模式之观察者模式

    可以帮你的对象知悉现况,不会错过该对象感兴趣的事。对象甚至在运行时可决定是否要继续被通知。 从报纸和杂志的订阅说起: 报社的业务就是出版报纸 向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订户,你就会一直收到新报纸。 当你不

    2024年01月24日
    浏览(41)
  • 重温设计模式 --- 观察者模式

    观察者模式 是一种行为型设计模式,它允许对象之间建立一种一对多的关系,使得当一个对象状态改变时,所有依赖它的对象都能够自动得到通知并更新自己的状态。该模式可以帮助我们实现松耦合的系统,以便更好地应对变化和扩展。 在观察者模式中,有两个角色: 观察

    2024年02月13日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包