vue 封装图片懒加载vue组件 图片懒加载原理

背景

图片懒加载(Lazy Loading)是一种优化网页性能的技术,它可以延迟网页中的图像加载时间,直到用户需要查看它们时才加载。这样可以减少页面加载时间,提高用户体验。

这是一个项目中实用的优化性能的功能,在很多前端组件库中,封装的图片组件都能看到有这一项功能

那么我们能不能像组件库中封装一个这样的组件,去手动实现这个功能,了解它的实现原理是什么?

原理

上面说到,懒加载就是等用户看到图片的时候才会加载,那么他的核心其实是怎么实现这个元素是否在可视区域中,不在可视区域时暂时不加载图片,等进入可视区域后在加载显示图片

可视区域方案一(offsetTop计算)

核心属性

拿到以下几个关键属性,然后在事件回调计算

1、获取目标元素 el.offsetTop 的 顶部位置偏移量

offsetTop:获取一个元素相对于其 offsetParent 元素的顶部位置偏移量。它返回的是一个整数值,表示该元素顶部相对于父元素的垂直距离,单位是像素。

2、获取文档根节点的 滚动条距离顶部的高度 document.documentElement.scrollTop

滚动条距离顶部的距离,当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0。兼容性 ie9+ 和其他主流浏览器。

3、获取文档根节点 可视区域高度

window.innerHeight:获取浏览器视口(Viewport)高度,即浏览器窗口中文档显示区域的高度(不包含地址栏、工具栏和状态栏等浏览器非内容区域)。对于移动设备,如手机或平板电脑,返回的通常是设备的屏幕高度,而不是浏览器的视口高度。兼容性 ie9+ 和其他主流浏览器。

document.documentElement.clientHeight:包含在浏览器 JavaScript 引擎中的属性,它返回当前文档在视口(viewport)中可见的高度,包括水平滚动条但不包括垂直滚动条的高度。与 window.innerHeight 属性类似。兼容性更好,ie6+ 和其他主流浏览器。

注意:offsetTop 是一个相对值,offsetTop 是相对其 offsetParent 元素的并不是相对浏览器窗口可视区域的。如果图片元素有 offsetParent 那么 offsetTop 是有偏差的。

offsetParent 元素指的是该元素的最近有定位属性(positionrelativeabsolutefixed)的祖先元素。如果元素自身就具有定位属性,那么 offsetParent 就是该元素本身。

因此,如果一个元素有父元素且父元素具有定位属性,那么它的 offsetTop 就是相对于父元素的位置。而如果该元素没有父元素或父元素没有定位属性,那么 offsetTop 就是相对于 body 元素的位置。

需要注意的是,如果元素跨越了多个具有定位属性的祖先元素,那么 offsetTop 将累加各级定位元素的高度,这可能会导致计算出错。

实现步骤

  1. 开始时将图像的 src 属性设置为一个空字符串或者一个占位符。
  2. 将图像的真实地址放在一个 data- 属性中,比如 data-src
  3. 使用 JavaScript 事件检测 图像元素 是否进入了可见区域。
  4. 当图像进入可见区域时,将 src 属性设置为 data-src 中的真实地址,从而触发图像加载。

封装组件

根据上面的思路以及属性,来封装一个简易的图片懒加载组件 ImageLazy/index.vue

<template>
  <div ref="imageLazy" class="imageLazy">
    <div v-if="error" class="error"></div>
    <img v-else ref="image" :src="realUrl" alt="" :data-src="url" class="img" @error="imageError" @load="imageLoad">
  </div>
</template>
<script>
export default {
  props: {
    url: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      realUrl: '', // 真实url
      error: false, // 显示展位图开关
      elOffsetTop: 0, // 图片元素距离顶部的偏移位置
    }
  },
  methods: {
    // 计算图片元素距离顶部的偏移位置
    getBoundingClientTop(el) {
      let top = el.offsetTop;
      let parent = el.offsetParent;
      while (parent) { // 循环迭代上级父级元素计算定位元素位置直至最顶层
        top += parent.offsetTop;
        parent = parent.offsetParent;
      }
      return top;
    },
    lazyload() {
      const imageOffsetTop = this.elOffsetTop // 图片元素距离顶部的偏移位置
      const elementScrollTop = document.documentElement.scrollTop // 滚动条距离顶部的距离,默认为0
      const clientHeight = document.documentElement.clientHeight || window.innerHeight // 文档可视区域高度(兼容写法)

      if (imageOffsetTop <= clientHeight + elementScrollTop) { // 图片距离顶部位置 小于或等于 可视区域高度+滚动条距离顶部高度
        // console.log('元素到达可视区域')
        this.error = false // 手动把占位图关闭
        this.$nextTick(() => {
          this.$refs.image.src = this.$refs.image.getAttribute("data-src"); // 把属性url传给真实url
          this.$refs.image.removeAttribute("data-src") // 加载完成后删除属性值

          this.removeEvent() // 加载完成后移除事件,避免浪费性能
        })
      }
    },
    // 图片加载失败回调
    imageError() {
      this.error = true
    },
    // 图片加载成功回调
    imageLoad() {
      this.error = false
    },
    // 移除事件
    removeEvent() {
      document.removeEventListener('scroll', this.lazyload)
    },
    // 添加事件
    addEvent() {
      document.addEventListener('scroll', this.lazyload)
    }
  },
  mounted() {
    this.addEvent()

    this.$nextTick(() => {
      this.elOffsetTop = this.getBoundingClientTop(this.$refs.imageLazy || this.$el)
    })
  },
  destroyed() {
    this.removeEvent()
  }
}
</script>
<style lang="less">
.imageLazy{
  width: 100%;
  height: 100%;
  .img{
    width: 100%;
    height: 100%;
  }
  .error{
    width: 100%;
    height: 100%;
    background-color: #C0C4CC;
  }
}
</style>

由于 offsetTop 是相对于父级定位元素计算的,所以这里要处理计算一下

在vue中使用该组件,设高度3000px,并且使用多个该组件情况

<template>
  <div id="app">
    <div class="footer-box">
      <div class="footer-img">
        <ImageLazy :url="imgUrl" />
      </div>
    </div>
    <div class="footer-box2">
      <div class="footer-img">
        <ImageLazy :url="imgUrl" />
      </div>
    </div>
  </div>
</template>
<script>
import ImageLazy from '@/components/ImageLazy/index.vue'
export default {
  name: "App",
  components: { ImageLazy },
  data() {
    return {
      imgUrl: 'http://files.jiangtao.ltd/10000.jpg',
      imgUr2: 'http://files.jiangtao.ltd/10000.jpg',
    };
  },
};
</script>
<style lang="less">
#app{
  display: flex;
  align-items: center;
  width: 100%;
  height: 3000px;
  position: relative;
}
.footer-box{
  width: 300px;
  height: 300px;
  position: relative;
  background-color: aquamarine;
  .footer-img{
    width: 100px;
    height: 100px;
  }
}
.footer-box2{
  position: absolute;
  bottom: 0;
  left: 0;
  width: 300px;
  height: 300px;
  background-color: aquamarine;
  .footer-img{
    width: 100px;
    height: 100px;
  }
}
</style>

效果图如下:

可以看到控制台地址变动,在地址替换后删除相关事件,避免浪费性能

GIF.gif

一个简易的图片懒加载组件便完成了,但是这种方式并不是太完美,还有需要优化的点:

1、滚动事件是否添加防抖或者节流,减少滚动事件触发的频率,但如果加了防抖或者节流,计算的位置的就会有偏差,如果不加可能在大应用里会有点占用性能

2、组件只对全局的滚动生效,意味着如果是在局部超出内容的滚动条内使用无法懒加载,还需要加上这种情况

3、是否可以把懒加载的显示时间提前一点,面对一些大图片时候,如果刚好卡在触发距离,可能会导致图片还没加载出来,所以可以考虑把距离提前一点,不用限制的那么死

这里暂时只实现一个基础实现方式,其余的可以自行优化

getBoundingClientRect

上文获取 目标图片元素距离文档顶部的位置 offsetTop 的方式比较麻烦,还要写一个方法专门计算叠加位置,其实可以替换成 getBoundingClientRect, 返回元素四个点的距离,left,top,right,bottom

兼容性 ie9+ (无法获取width,height属性)和其他主流浏览器。

可视区域方案二(IntersectionObserver观察者)

IntersectionObserver API:这是一种基于观察者模式的懒加载技术,可以在元素进入或离开视口时通知JavaScript,并调用相应的回调函数来处理图片加载。使用此方法需要在代码中使用 IntersectionObserver API,并初始化一个观察器对象来监听需要懒加载的图片元素。这个方法比较简单并且性能较高,但是需要浏览器支持此API。

实现步骤

  1. 开始时将图像的 src 属性设置为一个空字符串或者一个占位符。
  2. 将图像的真实地址放在一个 data- 属性中,比如 data-src
  3. 使用 JavaScript 事件检测 图像元素 是否进入了可见区域。
  4. 当图像进入可见区域时,将 src 属性设置为 data-src 中的真实地址,从而触发图像加载。

封装组件

<template>
  <div ref="imageLazy" class="imageLazy">
    <div v-if="error" class="error"></div>
    <img v-else ref="image" :src="realUrl" alt="" :data-src="url" class="img" @error="imageError" @load="imageLoad">
  </div>
</template>
<script>
export default {
  props: {
    url: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      realUrl: '',
      error: false,
      element: null, // 当前dom元素
      instanceObserver: null, // 观察者实例
    }
  },
  methods: {
    // 观察者回调
    callback(entries) {
       entries.forEach(entry => {
        if (entry.isIntersecting) { // 处理元素进入视口的操作
          this.error = false // 手动把占位图关闭
          this.$nextTick(() => {
            this.$refs.image.src = this.$refs.image.getAttribute("data-src"); // 把属性url传给真实url
            this.$refs.image.removeAttribute("data-src") // 加载完成后删除属性值

            this.instanceObserver.unobserve(this.element) // 加载后立即停止观察元素
          })
          console.log('进入可视区域')
        } else { // 处理元素离开视口的操作
          console.log('离开可视区域')
        }
      });
    },
    // 图片加载成功回调
    imageLoad() {
      this.error = false
    },
    // 图片加载失败回调
    imageError() {
      this.error = true
    },
  },
  mounted() {
    this.element = this.$refs.imageLazy || this.$el
    this.instanceObserver = new IntersectionObserver(this.callback) // 创建实例
    this.instanceObserver.observe(this.element) // 开始观察
  },
  destroyed() {
    this.instanceObserver.unobserve(this.element) // 销毁后停止观察元素
  }
}
</script>
<style lang="less">
.imageLazy{
  width: 100%;
  height: 100%;
  .img{
    width: 100%;
    height: 100%;
  }
  .error{
    width: 100%;
    height: 100%;
    background-color: #C0C4CC;
  }
}
</style>

GIF2.gif

如要提前触发加载图片,需要扩大监听区域可以通过创建实例的第二个参数项options配置,例如

this.instanceObserver = new IntersectionObserver(this.callback, {
  root: null,
  rootMargin: "0px 0px 100px 0px",
}) // 创建实例

结论

图片懒加载的实现思路步骤基本是一致的,两者相比,

一个是自定义JavaScrip事件的实现,兼容性好

一个是使用新的API,性能较好,但是在低版本主流游览器不支持,不支持IE

3e9432b249f476fc9b3531d1de257c53_1242x515.png

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务