# vue实现一个图片懒加载指令

参考ConardLi大佬 (opens new window)

图片的标签是img标签,图片的来源主要是src属性,浏览器是否发起加载图片的请求是根据是否有src属性决定的。所以可以从img标签的src属性入手,在没进到可视区域的时候,就先不给img标签的src属性赋值。src属性不为空时,即使img元素为display:none;浏览器也会去加载图片。 也可以把src的属性写成了about:blank,这是因为不能随便写一个不存在的地址,否则控制台会报错:加载失败,如果写成空或不写,那么它会认为src就是当前页面。如果写成about:blank,大家相安无事,并且不同浏览器兼容性好。

<img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2015/09/09/16/05/forest-931706_1280.jpg" alt="">
<img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2014/08/01/00/08/pier-407252_1280.jpg" alt="">
<img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2014/12/15/17/16/pier-569314_1280.jpg" alt="">
<img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2010/12/13/10/09/abstract-2384_1280.jpg" alt="">
<img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2015/10/24/11/09/drop-of-water-1004250_1280.jpg"
1
2
3
4
5

# 监听 scroll 事件判断元素是否进入视口

通过图片的offsetTopwindowinnerHeightscrollTop判断图片是否位于可视区域

var img = document.getElementsByTagName("img");
var n = 0; //存储图片加载到的位置,避免每次都从第一张图片开始遍历
lazyload(); //页面载入完毕加载可是区域内的图片
// 节流函数,保证每200ms触发一次
function throttle(event, time) {
    let timer = null;
    return function (...args) {
    if (!timer) {
        timer = setTimeout(() => {
        timer = null;
        event.apply(this, args);
        }, time);
    }
    }
}
window.addEventListener('scroll', throttle(lazyload, 200))
function lazyload() { //监听页面滚动事件
    var seeHeight = window.innerHeight; //可见区域高度
    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; //滚动条距离顶部高度
    for (var i = n; i < img.length; i++) {
        console.log(img[i].offsetTop, seeHeight, scrollTop);
        if (img[i].offsetTop < seeHeight + scrollTop) {
            if (img[i].getAttribute("src") == "loading.gif") {
            img[i].src = img[i].getAttribute("data-src");
            }
            n = i + 1;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  • 可能会存在下面几个问题:
    • 每次滑动都要执行一次循环,如果有1000多个图片,性能会很差
    • 每次读取 scrollTop 都会引起回流
    • scrollTopDOM的嵌套关系有关,应该根据getboundingclientrect获取

    Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。返回的结果是包含完整元素的最小矩形,并且拥有left, top, right, bottom, x, y, width, 和 height这几个以像素为单位的只读属性用于描述整个边框。除了widthheight 以外的属性是相对于视图窗口的左上角来计算的。当计算边界矩形时,会考虑视口区域(或其他可滚动元素)内的滚动操作,也就是说,当滚动位置发生了改变,topleft属性值就会随之立即发生变化(因此,它们的值是相对于视口的,而不是绝对的)。如果你需要获得相对于整个网页左上角定位的属性值,那么只要给topleft属性值加上当前的滚动位置(通过 window.scrollXwindow.scrollY),这样就可以获取与当前的滚动位置无关的值。

    • 滑到最后的时候刷新,会看到所有的图片都加载了

# IntersectionObserver

IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。当一个IntersectionObserver对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦IntersectionObserver被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。

创建一个 IntersectionObserver对象,并传入相应参数和回调用函数,该回调函数将会在目标(target)元素和根(root)元素的交集大小超过阈值(threshold)规定的大小时候被执行。

var observer = new IntersectionObserver(callback, options); // callback是可见性变化时的回调函数;options参数可选
1

返回的observer是一个观察器实例 实例的方法:

  • IntersectionObserver.observe()

开始监听一个目标元素

  • IntersectionObserver.unobserve()

停止监听特定的目标元素

  • IntersectionObserver.disconnect()

关闭监听器

所以图片懒加载代码如下:

var img = document.getElementsByTagName("img");
// 当监听的元素进入可视范围内的会触发回调
if (IntersectionObserver) {
    let lazyImageObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach((entry, index) => {  // entry其实就是每一个IntersectionObserverEntry对象,提供目标元素的信息,一共有六个属性。
            let lazyImage = entry.target;
            // 如果元素可见            
            if (entry.intersectionRatio > 0) {
                if (lazyImage.getAttribute("src") == "loading.gif") {
                    lazyImage.src = lazyImage.getAttribute("data-src");
                }
                lazyImageObserver.unobserve(lazyImage)
            }
        })
    })
    for (let i = 0; i < img.length; i++) {
        lazyImageObserver.observe(img[i]);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

TIP

callback一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。callback函数的参数(entries)是一个数组,每个成员都是一个IntersectionObserverEntry对象。因为后面遍历循环了图片数值,所以有多个被观察的对象。

# 实现无限滚动

var intersectionObserver = new IntersectionObserver(
  function (entries) {
    // 如果不可见,就返回
    if (entries[0].intersectionRatio <= 0) return;
    loadItems(10);
    console.log('Loaded new items');
  });

// 开始观察
intersectionObserver.observe(
  document.querySelector('.scrollerFooter')
);
1
2
3
4
5
6
7
8
9
10
11
12

无限滚动时,最好在页面底部有一个页尾栏。一旦页尾栏可见,就表示用户到达了页面底部,从而加载新的条目放在页尾栏前面。这样做的好处是,不需要再一次调用observe()方法,现有的IntersectionObserver可以保持使用。

# 实现vue自定义指令v-lazyload

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <style>
            img {
                width: 100%;
                height: 300px;
            }
        </style>
    </head>
    <body>
        <div id="app">
            <p v-for="item in imgs" :key="item">
                <img v-lazyload="item">
            </p>
        </div>
    </body>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <script>
        Vue.directive("lazyload", {
            // 指令的定义
            bind: function(el, binding) {
                let lazyImageObserver = new IntersectionObserver((entries, observer) => {
                    entries.forEach((entry, index) => {
                        let lazyImage = entry.target;
                        // 相交率,默认是相对于浏览器视窗
                        if(entry.intersectionRatio > 0) {
                            lazyImage.src = binding.value;
                            // 当前图片加载完之后需要去掉监听
                            lazyImageObserver.unobserve(lazyImage);
                        }

                    })
                })
                lazyImageObserver.observe(el);
            },
        });
        var app = new Vue({
            el: "#app",
            data: {
                imgs: [
                    'https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657907683.jpeg',
                    'https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657913523.jpeg',
                    'https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657925550.jpeg',
                    'https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657930289.jpeg',
                    'https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657934750.jpeg',
                    'https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657918315.jpeg',
                ]
            },
        });
    </script>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

代码参考牧码的星星 (opens new window)。自定义指令具体内容可以看vue官方文档