浅入浅出IntersectionObserver并实现一个简单的图片懒加载组件

在日常的业务开发当中,我们常常需要依赖DOM元素的可见性来完成某些需求,如图片的懒加载、数据列表的下拉加载等场景。

传统的实现方式

传统的/DOM元素的可见性检测方案大多数都是通过监听容器元素的scroll事件,然后获取目标元素坐标以及相关数据,最后才能判断当前元素是否可视。例如下面是一个判断div.target元素是否在他的容器div.container中可视的例子。

function Visible() {
  const [visible, setVisible] = useState(false);
  const targetRef = useRef<HTMLDivElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const targetHeight = targetRef.current.getBoundingClientRect().height;
    const containerHeight = containerRef.current.getBoundingClientRect().height;
    const scrollHeight = containerRef.current.scrollHeight;

    const onScroll = () => {
      if (scrollHeight - containerRef.current.scrollTop - containerHeight > targetHeight) {
        setVisible(false);
      } else {
        setVisible(true);
      }
    };

    containerRef.current.addEventListener('scroll', onScroll);

    return () => {
      containerRef.current.removeEventListener('scroll', onScroll);
    };
  }, []);

  return (
    <div>
      <div>target是否可见:{visible ? '是' : '否'}</div>
      <div
        ref={containerRef}
        style={{
          height: 300,
          overflow: 'auto',
        }}
        className="container">
        <div style={{ height: 500 }}></div>
        <div style={{ backgroundColor: 'yellow', height: 100 }} className="target" ref={targetRef}>
          target
        </div>
      </div>
    </div>
  );
}
2021-12-26 21-50-09 00_00_03-00_00_09.gif

我们知道,scroll事件的发生是十分密集的,而我们在监听scroll事件的回调函数中,还需要去获取容器的scrollTop这会导致“重排”的发生。此时需要我们额外去做一些防抖或是节流的工具,防止造成性能问题。

IntersectionObserver

IntersectionObserver 提供了一种异步观察目标元素在其祖先元素或顶级文档视窗(viewport)中是否可视的方法。

IntersectionObserver的用法十分简单,我们只需要定义好DOM元素的可视状态发生变化后需要做些什么,以及需要观察哪些元素的可视状态就好了。下面的代码同样完成可视检测的功能。

function Visible() {
  const [visible, setVisible] = useState(false);
  const targetRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let intersectionObserver = new IntersectionObserver(function (entries) {
      // 定义DOM元素的可视状态发生变化后需要做些什么
      if (entries[0].intersectionRatio > 0) {
        // intersectionRatio大于0,代表监听的元素由不可见变成可见
        setVisible(true);
      } else {
        // 反之则代表监听的元素由可见变成不可见
        setVisible(false);
      }
    });

    // 监听target元素的可见性
    intersectionObserver.observe(targetRef.current);

    return () => {
      intersectionObserver.unobserve(targetRef.current);
      intersectionObserver.disconnect();
      intersectionObserver = null;
    };
  }, []);

  return (
    <div>
      <div>target是否可见:{visible ? '是' : '否'}</div>
      <div
        style={{
          height: 300,
          overflow: 'auto',
        }}
        className="container">
        <div style={{ height: 500 }}></div>
        <div style={{ backgroundColor: 'yellow', height: 100 }} className="target" ref={targetRef}>
          target
        </div>
      </div>
    </div>
  );
}

使用intersectionObserver后,除了代码变得简短了之外,intersectionObserver构造函数中传入的回调函数只会在观察的元素的可视状态发生变化后才会执行,很好的解决传统判断可视的方案的性能瓶颈。

接下来我们详细的看看intersectionObserver这个API。

const intersectionObserver = new IntersectionObserver(callback, options?) ;

IntersectionObserver构造函数会接收两个参数。

callback

callback为被观察元素的可视状态发生变更后的回调函数,此回调函数接受两个参数:

function callback(entries, observer?) => {
  //...
}

entries:一个IntersectionObserverEntry对象的数组。IntersectionObserverEntry对象用于描述被观察对象的可视状态的变化,拥有以下的属性:

  • entry.boundingClientRect:被观察元素的边界信息,相当于被观察元素调用getBoundingClientRect()的结果。
  • entry.intersectionRatio:被观察元素与容器元素相交矩形面积与被观察元素总面积的比例。
  • entry.intersectionRect:相交矩形的边界信息。
  • entry.isIntersecting:一个布尔值,表示被观察元素是否可视,如果是true,则表示元可视,反之则表示不可视。
  • entry.rootBounds:容器元素的边界信息,相当于容器元素调用getBoundingClientRect()的结果。
  • entry.target:被观察的元素的引用。
  • entry.time:当前时间戳。

observer:当前IntersectionObserver实例的引用。

options

options为一个可选参数,可传入以下属性:

  • root:指定容器元素,默认为浏览器窗体元素。容器元素必须是目标元素的祖先节点。
  • rootMargin:用于扩展或缩小rootBounds的大小,用法与CSS中margin一致,默认值为默认值是"0px 0px 0px 0px"。
  • threshold:number或number数组,用于指定callback回调函数执行的阈值,如传入[0, 0.2, 0.6, 0.8, 1]时,intersectionRatio每增加或减少0.2时都会触发回调函数的执行。默认值为0。需要注意的时,由于回调函数时异步触发的,在回调函数执行时intersectionRatio可能已经和指定的阈值不一致了。
    2021-12-27 00-02-35 00_00_05-00_00_10.gif

IntersectionObserver实例

IntersectionObserver构造函数会把options中的属性挂载到IntersectionObserver实例上,并赋予IntersectionObserver实例四个方法:

  • IntersectionObserver.disconnect():停止监听工作。
  • IntersectionObserver.observe(targetElem):开始监听某个元素可视状态的变化。
  • IntersectionObserver.takeRecords():返回所有观察目标的IntersectionObserverEntry对象数组。
  • IntersectionObserver.unobserve(targetElem):停止监听某个目标元素。

使用IntersectionObserver实现一个简单的图片懒加载组件

实现思路

在页面加载时我们不会去加载img标签的图片资源,只有在img标签已经可视了,我们才会去加载图片资源。

实现代码

import { useEffect, useRef, useState } from 'react';

function LazyImg(props) {
  // 单独将src从props中提取出来
  const { src, ...restProps } = props;
  // 标记是否已经进入过可视范围
  const [loaded, setLoaded] = useState(false);
  const imgRef = useRef<HTMLImageElement>(null);

  useEffect(() => {
    let intersectionObserver = new IntersectionObserver(function (entries) {
      // 定义DOM元素的可视状态发生变化后需要做些什么
      if (entries[0].intersectionRatio > 0) {
        // intersectionRatio大于0,代表监听的元素由不可见变成可见
        setLoaded(true);

        // 加载过后,后续无需继续观察img的可视状态,进行解绑操作
        intersectionObserver.unobserve(imgRef.current);
        intersectionObserver.disconnect();
        intersectionObserver = null;
      }
    });

    // 监听target元素的可见性
    intersectionObserver.observe(imgRef.current);

    return () => {
      if (intersectionObserver) {
        intersectionObserver.unobserve(imgRef.current);
        intersectionObserver.disconnect();
        intersectionObserver = null;
      }
    };

    // src出现变化时需要重新进行绑定
  }, [src]);

  // 只有当loaded为true时才去加载src
  return <img {...restProps} ref={imgRef} src={loaded ? src : ''} />;
}

export default LazyImg;

实现效果

2021-12-27 08-52-03 00_00_02-00_00_09.gif
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容

  • 转载地址:https://segmentfault.com/a/1190000010744417懒加载什么是懒加载...
    秀逼阅读 481评论 0 0
  • 一、图片懒加载原理 浏览器是否发起请求图片是根据 中src的属性,所以实现原理就是在图片没有进入可视区域的时候将图...
    冰雪_666阅读 214评论 0 3
  • 懒加载的好处就不扯了,如何实现呢?早期处理... src是网络数据可直接复制 这里为什么使用data- 命名属性,...
    感觉不错哦阅读 596评论 0 1
  • 前言 在现在以用户体验至上的前端时代,为了提升页面加载速度,为了提升用户体验,我们经常会用到图片懒加载这个功能。所...
    郝晨光阅读 1,242评论 2 21
  • 懒加载 什么是懒加载 懒加载其实就是延迟加载,是一种对网页性能优化的方式,比如当访问一个页面的时候,优先显示可视区...
    grain先森阅读 2,620评论 0 79