sunilwang

V1

2022/11/22阅读:29主题:橙心

说说IntersectionObserver交叉观察器

前言

过去,要检测一个元素是否可见或者两个元素是否相交并不容易,很多解决办法不可靠或性能很差。然而,随着互联网的发展,这种需求却与日俱增,比如,下面这些情况都需要用到相交检测:

  • 图片懒加载——当图片滚动到可见时才进行加载
  • 内容无限滚动——也就是用户滚动到接近内容底部时直接加载更多,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉
  • 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况
  • 在用户看见某个区域时执行任务或播放动画

过去,相交检测通常要用到事件监听,并且需要频繁调用 `Element.getBoundingClientRect()`[1] 方法以获取相关元素的边界信息。事件监听和调用 `Element.getBoundingClientRect()`[2] 都是在主线程上运行,因此频繁触发、调用可能会造成性能问题。这种检测方法极其怪异且不优雅。

Intersection Observer API 会注册一个回调函数,每当被监视的元素进入或者退出另外一个元素时 (或者 viewport[3] ),或者两个元素的相交部分大小发生变化时,该回调方法会被触发执行。这样,我们网站的主线程不需要再为了监听元素相交而辛苦劳作,浏览器会自行优化元素相交管理。

用法

1. 介绍

Intersection Observer API 提供了一种异步检测目标元素与祖先元素或 viewport[4] 相交情况变化的方法

let io = new IntersectionObserver(callback, options);

上面代码中,IntersectionObserver是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数,option是配置对象(该参数可选)。

构造函数的返回值是一个观察器实例。实例的observe方法可以指定观察哪个 DOM 节点。

2. 方法

方法 说明
observe() 开始监听一个目标元素
unobserve() 停止监听特定目标元素
takeRecords() 返回所有观察目标的 IntersectionObserverEntry 对象数组
disconnect() 使 IntersectionObserver 对象停止全部监听工作
root 用来获取当前 intersectionObserver 实例的根元素(只读)
rootMargin 与 CSS 属性`margin`[5]语法相似的字符串 (string) 对象。在交叉检测开始之前,由rootMargin 规定的矩形的每一边都会被添加至`root`[6]元素的边框盒 的相应边(只读)
thresholds 一个包含阈值的列表,按升序排列,列表中的每个阈值都是监听对象的交叉区域与边界区域的比率(只读)
// 开始观察
io.observe(document.getElementById('example'));

// 停止观察
io.unobserve(element);

// 关闭观察器
io.disconnect();

上面代码中,observe的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。

io.observe(elementA);
io.observe(elementB);

3. callback 参数

callback 以下情况会被调用:

  • 每当目标 (target) 元素与设备视窗或者其他指定元素发生交集的时候执行
  • Observer 第一次监听目标元素的时候

接收一个参数 entries,即 IntersectionObserverEntry 实例。描述了目标元素与 root 的交叉状态。具体参数如下:

属性 说明
boundingClientRect 返回包含目标元素的边界信息,返回结果与 element.getBoundingClientRect() 相同
intersectionRatio 返回目标元素出现在可视区的比例
intersectionRect 用来描述 root 和目标元素的相交区域
isIntersecting 返回一个布尔值,下列两种操作均会触发 callback:1. 过渡从不相交到相交,返回 true。2. 过渡从相交到不相交,返回 false
rootBounds 用来描述交叉区域观察者(intersection observer)中的根.
target 目标元素:与根出现相交区域改变的元素 (Element)
time 表示相交更改发生的时间相对于文档创建的时间,单位为毫秒
isVisible 布尔值,目标元素是否可见(该属性还在试验阶段,不建议在生产环境中使用)

4. options 参数

options 是一个对象,控制观察者的回调函数的被调用时的环境,也可以不填。共有三个属性,具体如下:

属性 说明
root 指定根 (root) 元素,用于检查目标的可见性。必须是目标元素的父级元素。如果未指定或者为null,则默认为浏览器视窗
rootMargin 根 (root) 元素的外边距。类似于 CSS 中的 `margin`[7] 属性,比如 "10px 20px 30px 40px" (top, right, bottom, left)。如果有指定 root 参数,则 rootMargin 也可以使用百分比来取值。该属性值是用作 root 元素和 target 发生交集时候的计算交集的区域范围,使用该属性可以控制 root 元素每一边的收缩或者扩张。默认值为 0。
threshold 可以是单一的 number 也可以是 number 数组,target 元素和 root 元素相交程度达到该值的时候 IntersectionObserver 注册的回调函数将会被执行。默认值是 0 (意味着只要有一个 target 像素出现在 root 元素中,回调函数将会被执行)

5. 简单示例

代码如下:

<template>
    <div class="home">
        <div
            :class="['box', index === 3 ? 'bgc' : '']"
            v-for="(item, index) in 10"
            :key="index"
        >
</div>
    </div>
</template>

<script>
export default {
    name"HomeView",
    mounted() {
      // 获取要观察的DOM元素
        const domList = document.querySelectorAll(".bgc");
        this.change(domList);
    },
    methods: {
    change(domList) {
      // 获取观察器实例
        const observer = new IntersectionObserver(
            (entries) => {
                entries.forEach((entry) => {
                    console.log("============entry============");
                    console.log(entry);
                });
            }
        );
        domList.forEach((item) => {
           // 开始观察
            observer.observe(item);
        });
    },
    },
};
</script>
<style scoped>
.box {
    height300px;
}
.bgc {
    background#83f3bd;
}
.location1,
.location2 {
    position: fixed;
    right0;
    color#fff;
    background-color#2c3e50;
    padding12px 30px;
    border-radius4px 0 0 4px;
}
.location1 {
    top80px;
}
.location2 {
    top130px;
}
</style>

不设置 options 默认为浏览器视窗,结果如下图:

应用

1. 图片懒加载

<template>
    <div class="lazy-load">
        <div class="item" v-for="(item, index) in 10" :key="index">
            <img
                class="lazy-img"
                :src="
                    index === 4
                        ? 'https://pic5.58cdn.com.cn/union/n_v2ceedf7efa4df45e6b3ff6deb66dd1016_2f9594bb291b4132.jpg'
                        : ''
                "

                alt=""
                :lazy="
                    index === 5
                        ? 'https://pic7.58cdn.com.cn/union/n_v20387b69d577342b0865f016ab97f0ef7_1d0ae5d9dfc01f60.png'
                        : ''
                "

            />

        </div>
    </div>
</template>
<script>
export default {
    name"LazyLoad",
    data() {
        return {};
    },
    mounted() {
        const imgDomList = document.querySelectorAll(".lazy-img");
        this.lazyLoad(imgDomList);
    },
    methods: {
        // 图片懒加载
        lazyLoad(imgDomList, preHeight = -140) {
            const observer = new IntersectionObserver(
                (entries) => {
                    entries.forEach((entry) => {
                        // 过渡从不相交到相交
                        if (entry.isIntersecting) {
                            const target = entry.target;
                            const lazySrc = target.getAttribute("lazy");
                            const src = target.getAttribute("src");
                            if (!src && lazySrc) {
                                console.log("图片懒加载");
                                target.setAttribute("src", lazySrc);
                            }
                            observer.unobserve(target);
                        }
                    });
                },
                {
                    // 根元素面积向上缩小140px
                    rootMargin`0px 0px ${preHeight}px 0px`,
                }
            );
            imgDomList.forEach((item) => {
                observer.observe(item);
            });
        },
    },
};
</script>
<style scoped>
.item {
    height202px;
}
.lazy-img {
    width100%;
}
</style>

效果如图示:

2. 内容无限滚动

<template>
    <div class="more">
        <div>
            <div
                v-for="(item, index) in itemNumber"
                class="item-li"
                :key="index"
            >
</div>
        </div>
        <div id="more">加载更多...</div>
    </div>
</template>
<script>
export default {
    name"More",
    data() {
        return {
            itemNumber20,
        };
    },
    mounted() {
        // 获取加载更多DOM
        const MoreDom = document.getElementById("more");
        this.getMore(MoreDom);
    },
    methods: {
        // 加载更多
        getMore(MoreDom, preHeight = 200) {
            const observer = new IntersectionObserver(
                (entries) => {
                    entries.forEach((entry) => {
                        // 从不相交到相交
                        if (entry.isIntersecting) {
                            console.log('加载更多');
                            this.itemNumber += 5;
                        }
                    });
                },
                {
                    // 根元素面积向下扩大200px
                    rootMargin`0px 0px ${preHeight}px 0px`,
                }
            );
            // 开始观察
            observer.observe(MoreDom);
        },
    },
};
</script>
<style scoped>
.item-li {
    height70px;
    vertical-align: middle;
}
.item-li:nth-child(odd) {
    background#2ddf8f;
}
.item-li:nth-child(even) {
    background#83f3bd;
}
</style>

效果如图示:

3. 曝光埋点

<template>
    <!-- 横向 -->
    <swiper
        :slides-per-view="3"
        :space-between="50"
        @slideChange="onSlideChange"
        class="my-swiper"
    >

        <swiper-slide
            :id="index"
            class="my-swiper-slide"
            v-for="(item, index) in 5"
            :key="index"
            >
Slide {{ index }}</swiper-slide
        >

    </swiper>
</template>

<script>
import { swiper, swiperSlide } from "vue-awesome-swiper";
import "swiper/dist/css/swiper.css";
export default {
    name"SwiperView",
    components: {
        swiper,
        swiperSlide,
    },
    mounted() {
        const domList = document.querySelectorAll(".my-swiper-slide");
        this.change(domList);
    },
    methods: {
        change(domList) {
            const observer = new IntersectionObserver(
                (entries) => {
                    entries.forEach((entry) => {
                        if (entry.isIntersecting) {
                            console.log("交叉比例大于0");
                            const { target } = entry;
                            const isHaveTrack =
                                target.getAttribute("data-lbgtrack-done");
                            if (isHaveTrack) {
                                observer.unobserve(entry.target);
                            } else {
                                target.setAttribute("data-lbgtrack-done""1");
                                console.log("埋点");
                            }
                        }
                    });
                },
                {
                    // 相交程度为50%的时候触发回调函数
                    threshold: [0.5],
                }
            );
            domList.forEach((item) => {
                observer.observe(item);
            });
        },
        onSlideChange() {
            console.log("slide change");
        },
    },
};
</script>
<style scoped>
.my-swiper {
    margin-top100px;
    height300px;
    background#83f3bd;
}
.my-swiper1 {
    height100vh;
}
.my-swiper-slide{
    color#fff;
}
</style>

效果如图:

兼容性

当前浏览器对于IntersectionObserver的支持性:IntersectionObserver-canuse[8]

IntersectionObserver 的 polyfill

npm install intersection-observer

import 'intersection-observer';

总结

以上就是本次要介绍的全部内容了,总的来说,IntersectionObserver 使用方便快捷有效,所以 赶紧用起来吧~

参考资料

Intersection_Observer_API[9]

作者简介

高艳:猫奴一枚

参考资料

[1]

Element.getBoundingClientRect(): https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect

[2]

Element.getBoundingClientRect(): https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect

[3]

viewport: https://developer.mozilla.org/zh-CN/docs/Glossary/Viewport

[4]

viewport: https://developer.mozilla.org/zh-CN/docs/Glossary/Viewport

[5]

margin: https://developer.mozilla.org/zh-CN/docs/Web/CSS/margin

[6]

root: https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver/root

[7]

margin: https://developer.mozilla.org/zh-CN/docs/Web/CSS/margin

[8]

IntersectionObserver-canuse: https://caniuse.com/?search=IntersectionObserver

[9]

Intersection_Observer_API: https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API

分类:

前端

标签:

JavaScript

作者介绍

sunilwang
V1