Vue实现懒加载

112 阅读2分钟

对于那些含有大量图片资源的网站,会采用“按需加载”的方式,也就是当图片资源出现在视口区域内,才会被加载,这样可能会影响一丢丢用户体验,但是能大大节省网站的流量

一原理

图片懒加载技术主要通过监听图片资源容器是否出现在视口区域内,来决定图片资源是否被加载。

实现先将img标签的data-src链接设为同一张图片(默认图片),当js监听到该图片进入可视窗口时,再将实际地址应用

那么实现图片懒加载技术的核心就是如何判断元素处于视口区域之内

二需求

假设有100张图片需要加载,这里模拟10张

三不使用懒加载

<template>
  <div ref="picContainer">
    <img v-for="item in pics" :src="item">
  </div>
</template>
<script>
export default {
  data() {
    return {
      pics: [
        'https://img2.baidu.com/it/u=2054114667,923284312&fm=253&fmt=auto&app=138&f=JPEG?w=700&h=495',
        'https://img2.baidu.com/it/u=3909178502,660462293&fm=253&fmt=auto&app=138&f=JPEG?w=667&h=500',
        'https://img1.baidu.com/it/u=823556726,3909312789&fm=253&fmt=auto&app=138&f=GIF?w=576&h=360',
        'https://img2.baidu.com/it/u=1358770272,2747002171&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=889',
        'https://img0.baidu.com/it/u=1508642736,848814477&fm=253&fmt=auto&app=138&f=JPEG?w=617&h=500',
        'https://img2.baidu.com/it/u=797291547,729995872&fm=253&fmt=auto&app=138&f=JPEG?w=750&h=500',
        'https://img1.baidu.com/it/u=3281088149,3807028112&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=352',
        'https://img0.baidu.com/it/u=346365362,73186175&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500',
        "https://img0.baidu.com/it/u=341785578,4004138032&fm=253&fmt=auto&app=138&f=JPEG?w=700&h=497",
        'https://img1.baidu.com/it/u=4005594463,3623990680&fm=253&fmt=auto&app=138&f=JPEG?w=700&h=497',
      ],
    }
  },
}
</script>

首次加载,全部10张出来了

1663057426811.png

四使用懒加载

如何判断元素处于视口区域之内,首先要了解一些属性的定义:

window.innerHeight---返回窗口的文档显示区的高度,如果有垂直滚动条,也包括滚动条高度.(想象假设有一个笔筒,也就是文档的高度,不管笔筒里面的笔多高,window.innerHeight表示的就是笔筒的高度)

getBoundingClientRect().top--- 表示的是元素到视口顶部的高度

offsetTop 是一个只读属性,返回当前元素相对于 offsetParent 节点顶部边界的偏移像素值。也可以相当于getBoundingClientRect().top + scrollTop

假设你设置浏览器的视口为iphone5s(320px宽* 568px高),那么window.innerHeight以及document.documentElement.clientHeight为568px.html页面里面有2个div元素,第一个设置高为800px, 第二个设置为100px,那么在没有滚动之下,第一个的getBoundingClientRect().top为0px,而第二个的getBoundingClientRect().top为800px,如果滚动,那么第一个的top会变成负数,而第二个会小于800px,当第二个的getBoundingClientRect().top小于等于568px并且getBoundingClientRect().top + 他自身的高度大于等于0px时候,表示黄色出现在了屏幕视野里面

  <style>
    html,body {
      padding: 0px;
      margin: 0px;
    }
    
  </style>
  <div style="height: 800px;width: 100%; background: red" id="test1">
    1
  </div>
  <div style="width:100%; height:100px;background: yellow" id="test2">2</div>
window.addEventListener('scroll', () => {
  console.log(window.innerHeight,'window.innerHeight----')
  console.log(document.documentElement.clientHeight, 'document.documentElement.clientHeight---')
  console.log(document.getElementById('test1').getBoundingClientRect().top, 'test1')
  console.log(document.getElementById('test2').getBoundingClientRect().top, 'test2')
})

以下是scroll拉到最上面的一个打印 1663061251778.png

lazyLoad.vue组件代码:

<template>
  <div style="height: 100%;overflow-y: scroll" @scroll="scroll" ref="picContainer">
    <img v-for="item in pics" :src="initImg" style="height: 300px" :data-src="item" ref="img">
  </div>
</template>
<script>
export default {
  data() {
    return {
      innerHeight: 0,
    }
  },
  props: {
    pics: {
      default: [],
      required: true,
    },
    initImg: {
      default: '',
      required: true,
    }
  },
  methods: {
    scroll() {
      this.debounce(this.handle, 300)()
    },
    debounce(fn, delay) {
      let timer = null
      // 每移动一次,就创建一个setTimeOut,然后进行清除
      return function () {
        if (timer) {
          console.log('清除');
          clearTimeout(timer)
        }
        timer = setTimeout(()=> {
          // fn 去调用this的方法  
          fn.call(thisarguments)
        },delay)   
      }
    },
    handle() { 
      // 1.先获取屏幕的高度
      this.innerHeight = window.innerHeight
      // 2.获取页面的所有的img元素
      this.$refs.img.forEach(item => {
        // 3.获取每个元素的getBoundingClientRect()
        const bound = item.getBoundingClientRect()
        // 4.判断元素是不是在屏幕内(假设bound.top为负数,加上他自身高度,如果大于0,说明元素后半截是在屏幕内的,&& bound.top + bound.height >0可以不要)
        if (bound.top < this.innerHeight && bound.top + bound.height >0) {
          item.setAttribute('src', item.dataset.src)
        }
      })
    }
  },
  mounted() {
    this.handle()
  }
}
</script>

页面使用代码:

<template>
   <div style="height: 100%;overflow-y: scroll">
    <lazyLoad
      :pics="pics"
      :init-img="initImg"
    />
  </div>
</template>
<script>
import lazyLoad from '../components/lazyLoad.vue'
export default {
  components: {
    lazyLoad
  },
  data() {
    return {
      pics: [
        'https://img2.baidu.com/it/u=2054114667,923284312&fm=253&fmt=auto&app=138&f=JPEG?w=700&h=495',
        'https://img2.baidu.com/it/u=3909178502,660462293&fm=253&fmt=auto&app=138&f=JPEG?w=667&h=500',
        'https://img1.baidu.com/it/u=823556726,3909312789&fm=253&fmt=auto&app=138&f=GIF?w=576&h=360',
        'https://img2.baidu.com/it/u=1358770272,2747002171&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=889',
        'https://img0.baidu.com/it/u=1508642736,848814477&fm=253&fmt=auto&app=138&f=JPEG?w=617&h=500',
        'https://img2.baidu.com/it/u=797291547,729995872&fm=253&fmt=auto&app=138&f=JPEG?w=750&h=500',
        'https://img1.baidu.com/it/u=3281088149,3807028112&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=352',
        'https://img0.baidu.com/it/u=346365362,73186175&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500',
        "https://img0.baidu.com/it/u=341785578,4004138032&fm=253&fmt=auto&app=138&f=JPEG?w=700&h=497",
        'https://img1.baidu.com/it/u=4005594463,3623990680&fm=253&fmt=auto&app=138&f=JPEG?w=700&h=497',
      ],
      initImg:'https://img0.baidu.com/it/u=1351006615,3296420961&fm=253&fmt=auto&app=120&f=JPEG?w=499&h=500'
    }
  },
}
</script>

测试15.gif

五如果图片的样式ui尺寸是一样的,也可以采用

懒加载组件:

<template>
  <div style="height: 100%;overflow-y: scroll" @scroll="scroll" ref="picContainer">
    <img v-for="item in pics.slice(0, picLength)" :src="item" :style="`height: ${itemHeight}px`" :data-src="item">
  </div>
</template>
<script>
export default {
  props: {
    pics: {
      required: true,
      default: []
    },
    itemHeight: {
      default: 200
    },
  },
  data() {
    return {
      picLength: 0,
      clientHeight: 0
    }
  },
  methods: {
    scroll() {
      // 1.获取卷出去的距离
      const scrollTop = this.$refs.picContainer.scrollTop
      // 2.卷出去 + 屏幕高度 = 已经显示的张数的高度 表示已经到低了,但是不要等完全到底再加载,可以给前面加一个阈值再加载
      if (scrollTop + this.clientHeight + 10 >= this.picLength * this.itemHeight) {
        // 如果目前显示的图片数量 和 总图片数量保持一致, 则不做任何处理
        if (this.picLength >= this.pics.length) return
        this.picLength ++
      }
    },
  },
  created() {
    this.clientHeight = document.documentElement.clientHeight;
    this.picLength = Math.ceil(this.clientHeight / this.itemHeight)
  }
}
</script>

页面使用:

<template>
   <div style="height: 100%;overflow-y: scroll">
    <lazyLoad
      :pics="pics"
      :item-Height="400"
    />
  </div>
</template>
<script>
import lazyLoad from '../components/lazyLoad.vue'
export default {
  components: {
    lazyLoad
  },
  data() {
    return {
      pics: [
        'https://img2.baidu.com/it/u=2054114667,923284312&fm=253&fmt=auto&app=138&f=JPEG?w=700&h=495',
        'https://img2.baidu.com/it/u=3909178502,660462293&fm=253&fmt=auto&app=138&f=JPEG?w=667&h=500',
        'https://img1.baidu.com/it/u=823556726,3909312789&fm=253&fmt=auto&app=138&f=GIF?w=576&h=360',
        'https://img2.baidu.com/it/u=1358770272,2747002171&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=889',
        'https://img0.baidu.com/it/u=1508642736,848814477&fm=253&fmt=auto&app=138&f=JPEG?w=617&h=500',
        'https://img2.baidu.com/it/u=797291547,729995872&fm=253&fmt=auto&app=138&f=JPEG?w=750&h=500',
        'https://img1.baidu.com/it/u=3281088149,3807028112&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=352',
        'https://img0.baidu.com/it/u=346365362,73186175&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500',
        "https://img0.baidu.com/it/u=341785578,4004138032&fm=253&fmt=auto&app=138&f=JPEG?w=700&h=497",
        'https://img1.baidu.com/it/u=4005594463,3623990680&fm=253&fmt=auto&app=138&f=JPEG?w=700&h=497',
      ],
      initImg: 'https://img0.baidu.com/it/u=1351006615,3296420961&fm=253&fmt=auto&app=120&f=JPEG?w=499&h=500'
    }
  },
}
</script>

六 原生

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <img src="https://img0.baidu.com/it/u=1351006615,3296420961&fm=253&fmt=auto&app=120&f=JPEG?w=499&h=500" alt="" data-src="https://img2.baidu.com/it/u=2054114667,923284312&fm=253&fmt=auto&app=138&f=JPEG?w=700&h=495">
  <img src="https://img0.baidu.com/it/u=1351006615,3296420961&fm=253&fmt=auto&app=120&f=JPEG?w=499&h=500" alt="" data-src="https://img2.baidu.com/it/u=3909178502,660462293&fm=253&fmt=auto&app=138&f=JPEG?w=667&h=500">
  <img src="https://img0.baidu.com/it/u=1351006615,3296420961&fm=253&fmt=auto&app=120&f=JPEG?w=499&h=500" alt="" data-src="https://img1.baidu.com/it/u=823556726,3909312789&fm=253&fmt=auto&app=138&f=GIF?w=576&h=360">
  <img src="https://img0.baidu.com/it/u=1351006615,3296420961&fm=253&fmt=auto&app=120&f=JPEG?w=499&h=500" alt="" data-src="https://img2.baidu.com/it/u=1358770272,2747002171&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=889">
  <img src="https://img0.baidu.com/it/u=1351006615,3296420961&fm=253&fmt=auto&app=120&f=JPEG?w=499&h=500" alt="" data-src="https://img0.baidu.com/it/u=1508642736,848814477&fm=253&fmt=auto&app=138&f=JPEG?w=617&h=500">
  <img src="https://img0.baidu.com/it/u=1351006615,3296420961&fm=253&fmt=auto&app=120&f=JPEG?w=499&h=500" alt="" data-src="https://img2.baidu.com/it/u=797291547,729995872&fm=253&fmt=auto&app=138&f=JPEG?w=750&h=500">
  <img src="https://img0.baidu.com/it/u=1351006615,3296420961&fm=253&fmt=auto&app=120&f=JPEG?w=499&h=500" alt="" data-src="https://img1.baidu.com/it/u=3281088149,3807028112&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=352">
  <img src="https://img0.baidu.com/it/u=1351006615,3296420961&fm=253&fmt=auto&app=120&f=JPEG?w=499&h=500" alt="" data-src="https://img0.baidu.com/it/u=346365362,73186175&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500">
  <img src="https://img0.baidu.com/it/u=1351006615,3296420961&fm=253&fmt=auto&app=120&f=JPEG?w=499&h=500" alt="" data-src="https://img0.baidu.com/it/u=341785578,4004138032&fm=253&fmt=auto&app=138&f=JPEG?w=700&h=497">
  <img src="https://img0.baidu.com/it/u=1351006615,3296420961&fm=253&fmt=auto&app=120&f=JPEG?w=499&h=500" alt="" data-src="https://img1.baidu.com/it/u=4005594463,3623990680&fm=253&fmt=auto&app=138&f=JPEG?w=700&h=497">
  <style>
    img {
      width: 100%;
      height: 300px;
    }
  </style>
</head>
<body>
  <script>
    window.addEventListener('scroll', () => {
      this.lazyLoad()
    })
    function lazyLoad(){
      const imgs = document.getElementsByTagName('img')
      console.log(Array.isArray(imgs), imgs.length)
      const clientHeight = document.documentElement.clientHeight
      Array.from(imgs).forEach(img => {
        const clientT = document.documentElement.scrollTop || document.body.scrollTop; // 卷上去的距离
        const singleImg = img.offsetTop
        const diff = singleImg - clientT
        if (diff < clientHeight) {
          img.src = img.dataset.src;
        }
      });
    }
    this.lazyLoad()
  </script>
</body>
</html>

其中也是利用,元素到屏幕顶部的距离(img.offsetTop - clientT )< 屏幕高度,这里的offsetTop与getBoundingClientRect().top的区别就是offsetTop包括了卷出去的部分,而getBoundingClientRect().top就是指到屏幕顶部的高度