自定义实现图片懒加载

650 阅读3分钟

实现这种图片懒加载方法其实有多种方式

  • 将每个img元素的top值与屏幕的高度作比较,在的话,就替换成要加载的图片。
  • 直接调用window上的IntersectionObserverAPI,目前大多数浏览器已支持。可以在回调中直接调用API就可以知道当前这张图片在不在屏幕中。

第一种方法涉及到的知识点

  1. 自定义属性
  2. 节流
  3. 图片在屏幕的top值与屏幕高度作比较

第二种方法涉及到的知识点

主要是熟悉window上的几个API

  1. IntersectionObserver,为构造函数
  2. IntersectionObserverEntry,为构造函数
  3. intersectionRatio:IntersectionObserverEntry.prototype上的属性

第一种方法整体思路

444.png

  1. 搞几张图片,src属性和data-src属性的值提前写好,如果是接口返回的,在返回成功后赋值即可。
  2. 对每一张图片都要去做判断,图片在屏幕的top值是否< 屏幕高度。
  3. 这里需要优化一下,因为有的图片加载完成了之后,就不用去判断了,所以可以重新获取一下这个imgList列表做循环。
  4. imgList.length === 0时,移除监听。
<template>
  <div>
    <div class="container" v-for="(item, i) in imgsList" :key="i">
      <img class="lazy" :src="item.default" :data-src="item.showPath" />
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      flag: false,
      imgArr: [],
    };
  },
  computed: {
   // 提前把数据写死
    imgsList() {
      let arr = [],
        brr = new Array(7);
      brr.fill(0);
      arr = brr.map((e, i) => {
        return {
          default: require(`./imgs/8.webp`),
          showPath: require(`./imgs/${i + 1}.webp`),
        };
      });
      return arr;
    },
  },
  created() {
    this.showImgs(); // 默认去加载屏幕中的图片
    window.addEventListener("scroll", _.throttle(this.showImgs,200)); // 滚动的时候也去加载
  },
  mounted() {
   // 获取需要懒加载的图片
    this.imgArr = document.getElementsByClassName("lazy");
    // 类数组转换为数组
    this.imgArr = [].slice.call(this.imgArr, 0);
    // console.log(this.imgArr.classList);
  },
  methods: {
    showImgs() {
      this.imgArr.forEach((e) => {
      // -300是为了好观察效果
        if (e.getBoundingClientRect().top <= window.innerHeight - 300) {
          // console.log(e.classList);
          // classList是img元素上的class属性的集合,是一个数组,采用remove方法去移除相关类名。
          // 把找到的img元素上的lazy类名移除。
          e.classList.remove("lazy");
          // 自定义属性赋值
          e.src = e.dataset.src;
          /* console.log(this.imgArr,e);// 第一个img是去除掉的,e=>去除掉class的e
         */
          this.imgArr = this.imgArr.filter((el) => el !== e);
          if (this.imgArr.length === 0) {
            // 加载完成后,移除scroll事件
            window.removeEventListener("scroll", this.showImgs);
          }
        }
      });
      // this.imgArr.forEach((e)=>{

      // })
    },
  },
};
</script>

<style lang='stylus' scoped>
.container {
  height: 300px;

  img {
    height: 100%;
  }
}
</style>

有一个地方需要注意:为什么可以拿到未删除.lazy属性的集合?

this.imgArr = this.imgArr.filter((el) => el !== e);

可以看下面这个例子:

<div><img src="" alt="" class="lazy"></div>
<div><img src="" alt="" class="lazy"></div>
<div><img src="" alt="" class="lazy"></div>
<div><img src="" alt="" class="lazy"></div>
<div><img src="" alt="" class="lazy"></div>
let imgs = document.getElementsByClassName('lazy')
imgs = [].slice.call(imgs,0) // imgs存的是DOM元素集合[<img/>,<img/>,...]
imgs.forEach((e,i)=>{
    if(i === 0){
        e.classList.remove('lazy')
        console.log(e)
        let brr = imgs.filter((el)=> {
            console.log('e',e)
            console.log('el',el)
            el !== e
            // 为什么两者可以相互比较,因为存储的是DOM元素,不是对象。
        })
    }
})

333.png

已删除的,在原数组中通过比较过滤出不一样的。

节流和防抖

在触发滚动事件中,可以使用节流,在200ms就去执行一次showImgs()方法。

下面来说一下节流和防抖的原理实现。

节流:在xx时间段内执行一次。如果你在这个时间段内去触发事件,则直接return.可以实现就是,每隔多长时间需要去干嘛。

防抖:在xx时间段内执行一次。但是如果你在这个时间段内去触发事件,则直接从头开始。比如说一个按钮,如果在5s内如果你一会点击,则最后一次点击之后的5s之后会执行,之前的都不算。可以实现一些需求,比如,一直滚动,等你不滚动的时候,去干些啥。

    <button id="btn">点击</button>
let btn = document.getElementById('btn')
// btn.addEventListener('click', throttle(fn, 2000, 20, 30))
btn.addEventListener('click', debounce(fn, 2000, 20, 30))

节流:

function throttle(fun, time, ...args) {
            let timer = null
            return function proxy() {
                if (timer) {
                    // timer有值直接return
                    return
                }           
                // timer: timeoutID,是一个正整数,表示定时器的编号
                timer = setTimeout(() => {
                    fun(...args)
                    timer = null //执行完之后就可以继续点击了
                }, time)
            }
        }

防抖:

function debounce(fun,time,...args){
            let timer = null
            return function proxy(){
                if(timer){
                    // 如果有定时器则清空掉,重新计时
                    clearTimeout(timer)
                }
                timer = setTimeout(()=>{
                    fun(...args)
                    timer = null
                },time)
            }
        }

第二种实现方式代码

methods:{
    lazyLoad() {
      console.time() //计算代码运行时间
      // 判断浏览器兼容性
      if (
        "IntersectionObserver" in window &&
        "IntersectionObserverEntry" in window &&
        "intersectionRatio" in window.IntersectionObserverEntry.prototype
      ) {
     // 创建IntersectionObserver实例,并传递匿名回调函数     
        let lazyImageLoader = new IntersectionObserver(function (
          entries,
          observer
        ) {
          // console.log(entries); // 每一张图片的位置信息
          entries.forEach((e) => {
          // 判断图片是否在视窗中
            if (e.isIntersecting) {
              // console.log('yes')
              let lazyImage = e.target;
                lazyImage.src = lazyImage.dataset.src;
                // 加载后清除类名,并且取消监控
                lazyImage.classList.remove("lazy");
                lazyImageLoader.unobserve(lazyImage);
              
            }
          });
          
        });
        
        console.timeEnd();// 0.04736328125 ms
        this.imgArr.forEach((e) => {
          // console.log(lazyImageLoader)
          lazyImageLoader.(e); //没有返回值,给每一张图片增加监控
          /* 目前理解就是IntersectionObserver.prototype.observe方法可以执行 
          new IntersectionObserver(function(){...})的回调 */
        });
      }
    },
}

在项目中判断在视口才去发请求

// 根据id来获取DOM,在调取observe()方法的时候,传的参数要是一个element,也就是一个DOM元素。
checkGetQues: _.throttle(function (ele, callback) {   
      {     
        if ("IntersectionObserver" in window &&
          "IntersectionObserverEntry" in window &&
          "intersectionRatio" in window.IntersectionObserverEntry.prototype) {
          const lazyImageLoader = new IntersectionObserver((entries, observer) => {
           // console.log('entries', entries)
            if (entries[0].isIntersecting) {
              callback();
              // 解除监测
              lazyImageLoader.unobserve(ele)
            }
          }, {
            rootMargin: "100px"
          })
          lazyImageLoader.observe(ele)
        }

      }
    }, 1000),

注意点:只要创建了这个对象new IntersectionObserver(),调用了observe()方法,window对象会自动去检测,不用你自己去写滚动方法.

最后

在真实项目中,可以借用第三方库lodash来实现节流和防抖需求。

btn.onclick = _.throttle(fn, 1000); 
btn.onclick = _.debounce(fn, 1000);

在vue中使用节流和防抖:

handleScroll: _.throttle(function () {
      // dosomething
    }, 200),
  },