阅读 1208

基于 vue 自定义指令实现图片懒加载

前言

在实际的项目中还是有很多图片懒加载的需求,尤其是c端的应用,在一些列表页,像商品列表等含有大量图片展示。我们能做的一个优化项就是对未进入当前视口的图片元素,延迟加载,减少一进入页面时的请求数。

图片的懒加载从实现上来说,就是一开始不设置 img 元素的 src,在上滑过程中,当图片滚动到可见时才进行加载,触发 src 设置。

这里暂时只介绍在 vue 中基于自定义指令如何实现,原生 js 或者 react 中大同小异,主要是实现的思路

常用的实现方式:

  • 监听 onscroll 滚动事件
  • 借助 IntersectionObserver

实现

一、监听 onscroll 事件

通过监听 onscroll 滚动事件,当滚动事件触发时,判断 el 元素是否进入可视区域。

关键是如何判断元素进入可视区域?

我们可以康一康 getBoundingClientRect 方法,MDN 上解释如为:

Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。包含当前元素的 left, top, right, bottom, x, y, width, 和 height这几个以像素为单位的只读属性

视口默认为屏幕左上角,加载 5 张图片来看一下打印的 top 值 每张图片的宽高为 300px,不难看出 top 值就是元素到屏幕顶部距离,慢慢向上滑动,top 的值是随着滑动距离递减的。于是我们可以得出简单结论:

  • top < 当前视口高度时,可以判断元素进入视口可见区域,获取视口高度可以结合 documentElement.clientHeightbody.clientHeight,具体的实现看项目的兼容性要求,这里只给出大致思路。

判断元素达到可视区域的代码参考如下:

/**
 * 判断元素是否进入可视区域
 */
export const isElementInViewport = el => {
  if (typeof el.getBoundingClientRect !== 'function') {
    return true
  }

  const clientHeight = _getClientHeight()
  const rect = el.getBoundingClientRect()
  return rect.top < clientHeight
}

// 获取视口高度
const _getClientHeight = () => {
  const dClientHeight = document.documentElement.clientHeight
  const bodyClientHeight = document.body.clientHeight
  let clientHeight = 0

  if (bodyClientHeight && dClientHeight) {
    clientHeight = bodyClientHeight < dClientHeight ? bodyClientHeight : dClientHeight
  } else {
    clientHeight = bodyClientHeight > dClientHeight ? bodyClientHeight : dClientHeight
  }

  return clientHeight
}
复制代码

监听滚动处理

// 创建 map 来缓存
window.lazyMap = new Map()

// 监听滚动事件,添加节流处理
window.onscroll = throttle(() => {
  window.lazyMap.forEach((lazyImg, key) => {
    if (isElementInViewport(lazyImg.el)) {
      lazyImg.el.src = lazyImg.value.src
      lazyImg.value.callback(lazyImg.el)
      window.lazyMap.delete(key)
    }
  })
}, 200)
复制代码

二、借助 IntersectionObserver

过去,要检测一个元素是否可见或者两个元素是否相交并不容易,很多解决办法不可靠或性能很差,就比如第一种方法,需要监听页面滚动,对性能多少会有点影响

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

相交检测可以发挥作用的地方:

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

关于 IntersectionObserver 的更多说明和用法 传送门

这里用到的一个关键属性 isIntersecting ,这是一个布尔值,指示目标元素是否转换为交集状态(true)或脱离交集状态(false)。当相交时,这个属性的值就为 true。相比之前的是否进入可视区域的判断,简单了不是一点半点🌹

核心代码为

window.observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      let lazyImage = entry.target
      // 判断是否相交
      if (entry.isIntersecting) {
        const src = lazyImage.getAttribute('data-src')
        lazyImage.src = src
        lazyImage.style.opacity = 1
        lazyImage.style.display = 'block'
        // 移除监听
        window.observer.unobserve(lazyImage)
      }
    })
  })
复制代码

自定义指令

好了,说回自定义指令,如果对自定义指令的用法不太熟悉请参考 Vue 官方文档 传送门

主要是要利用好这几个钩子函数:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    • 比如监听 onerror 事件,加载失败后设置 default src。
    • 设置 onscroll 监听
    • IntersectionObserver 的初始化
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
    • onscroll 方案,如果 el isInViewport,则设置 src,否则将 elbinding 添加到 lazyMap
    • IntersectionObserver 方案只需要调用 observe 监听目标对象
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
    • 同 inserted,只不过需要做一次 src 的判断,如果 valueoldValue 一致,不继续往下执行
  • unbind:只调用一次,指令与元素解绑时调用。
    • onscroll 方案, 将当前目标元素从 lazyMap 移除
    • IntersectionObserver 方案,调用 unobserve 移除目标对象监听

最终效果

总结

存在必有其合理性,就目前来说,两种方案都有它们的优缺点

方案优点缺点
基于 onscroll兼容性好性能不佳,需要持续监听滚动事件;实现不太优雅,代码量大
IntersectionObserver性能佳,实现优雅,代码量少兼容性差,IE 完全不支持,在 safari 也需要 12 以上才支持

MDN 这篇文章上对这点说得很好,有兴趣的可以看下 传送门

最后

如有不太合理的地方,希望及时指出,多多交流~

讲的总是空洞的,实践出真知,查看代码请各位童鞋移步 github,别忘记点个赞哟 👍

项目地址 👉 :github.com/MrLeihe/vue…

参考资料

文章分类
前端
文章标签