图片懒加载的前世此生

1、前言

  一般状况下,HTML 中的图片资源会自上而下依次加载,而部分图片只有在用户向下滚动页面的场景下才能被看见,不然这部分图片的流量就白白浪费了。html

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

  而上述“按需加载”的方式就是今天的主角 -- 图片懒加载技术git

2、原理

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

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

3、前世

  早前实现的思路:闭包

  • 给目标元素指定一张占位图,将真实的图片连接存储在自定义属性中,一般是data-src;
  • 监听与用户滚动行为相关的 scroll 事件;
  • 在 scroll 事件处理程序中利用 Element.getBoundingClientRect() 方法判断目标元素与视口的交叉状态;
  • 当目标元素与视口的交叉状态大于0时,将真实的图片连接赋给目标元素 src 属性或者 backgroundImage 属性。

一、scroll 事件

  scroll 事件可能会被高频度的触发,而按照上述思路,必然会在 scroll 事件处理程序中出现大量的 DOM 操做,这极可能会使页面再也不“如丝般顺滑”,这时就须要下降 DOM 操做的频率。app

  下降 DOM 操做的频率能够采用函数节流的方式,函数节流可以确保在固定时间间隔只执行一次操做。异步

  与函数节流相关的另外一个概念叫作函数防抖,这位兄弟一样等待一个时间间隔执行操做,可是它被打断以后就须要从新开始计时。函数

  这也是此场景下采用函数节流的缘由。在 JavaScript 中能够采用 setTimeout + 闭包的方式实现函数节流:性能

function throttle (fn, interval = 500) {
  let timer = null
  let firstTime = true

  return function (...args) {
    if (firstTime) {
      // 第一次加载
      fn.apply(this, args)
      return firstTime = false
    }

    if (timer) {
      // 定时器正在执行中,跳过
      return
    }

    timer = setTimeout(() => {
      clearTimeout(timer)
      timer = null
      fn.apply(this, args)
    }, interval)

  }
}
复制代码

  除了上述 setTimeout + 闭包的实现方式,还能够经过 window.requestAnimationFrame() 方法实现,这里再也不赘述,有兴趣的同窗能够本身尝试尝试。

二、getBoundingClientRect()方法

  JavaScript 提供 Element.getBoundingClientRect() 方法返回元素的大小以及相对于视口的位置信息,接下来会用到返回对象的四个属性:

  • top 和 left 是目标元素左上角坐标与网页左上角坐标的偏移值;
  • width 和 height 是目标元素自身的宽度和高度。

  再结合视口的高度和宽度,便可判断元素是否出如今视口区域内:

function isElementInViewport (el) {
  const { top, height, left, width } = el.getBoundingClientRect()
  const w = window.innerWidth || document.documentElement.clientWidth
  const h = window.innerHeight || document.documentElement.clientHeight
  return (
    top <= h &&
    (top + height) >= 0 &&
    left <= w &&
    (left + width) >= 0
  )
}
复制代码

三、实现

  接下来在实现图片懒加载的过程当中,还须要注意一些小问题:

  • scroll 事件只有在滚动行为发生时,才会被触发,这里须要手动加载一次首屏的图片;
  • 利用 addEventListener 注册事件处理程序时,须要保存事件处理程序的引用,以便销毁注册的事件程序。
function LazyLoad (el, options) {
  if (!(this instanceof LazyLoad)) {
    return new LazyLoad(el)
  }

  this.setting = Object.assign({}, { src: 'data-src', srcset: 'data-srcset', selector: '.lazyload' }, options)

  if (typeof el === 'string') {
    el = document.querySelectorAll(el)
  }
  this.images = Array.from(el)

  this.listener = this.loadImage()
  this.listener()
  this.initEvent()
}

LazyLoad.prototype = {
  loadImage () {
    return throttle(function () {
      let startIndex = 0
      while (startIndex < this.images.length) {
        const image = this.images[startIndex]
        if (isElementInViewport(image)) {
          const src = image.getAttribute(this.setting.src)
          const srcset = image.getAttribute(this.setting.srcset)
          if (image.tagName.toLowerCase() === 'img') {
            if (src) {
              image.src = src
            }
            if (srcset) {
              image.srcset = srcset
            }
          } else {
            image.style.backgroundImage = `url(${src})`
          }
          this.images.splice(startIndex, 1)
          continue
        }
        startIndex++
      }
      
      if (!this.images.length) {
        this.destroy()
      }
    }).bind(this)
  },
  initEvent () {
    window.addEventListener('scroll', this.listener, false)
  },
  destroy () {
    window.removeEventListener('scroll', this.listener, false)
    this.images = null
    this.listener = null
  }
}
复制代码

4、此生

  现在,Web为开发者提供了 IntersectionObserver 接口,它能够异步监听目标元素与其祖先或视窗的交叉状态,注意这个接口是异步的,它不随着目标元素的滚动同步触发,因此它并不会影响页面的滚动性能。

  IntersectionObserver 构造函数接收两个参数,回调函数以及配置项。

一、配置项

  配置项中的参数有如下三个:

  • root:所监听对象的具体祖先元素,默认是 viewport ;
  • rootMargin:计算交叉状态时,将 margin 附加到祖先元素上,从而有效的扩大或者缩小祖先元素断定区域;
  • threshold:设置一系列的阈值,当交叉状态达到阈值时,会触发回调函数。

二、回调函数

  IntersectionObserver 实例执行回调函数时,会传递一个包含 IntersectionObserverEntry 对象的数组,该对象一共有七大属性:

  • time:返回一个记录从 IntersectionObserver 的时间原点到交叉被触发的时间的时间戳;
  • target:目标元素;
  • rootBounds:祖先元素的矩形区域信息;
  • boundingClientRect:目标元素的矩形区域信息,与前面提到的 Element.getBoundingClientRect() 方法效果一致;
  • intersectionRect:祖先元素与目标元素相交区域信息;
  • intersectionRatio:返回intersectionRect 与 boundingClientRect 的比例值;
  • isIntersecting:目标元素是否与祖先元素相交。

三、实现

  在此以前,还须要了解 IntersectionObserver 实例方法:

  • observe:开始监听一个目标元素;
  • unobserve:中止监听特定的元素;
  • disconnect:使 IntersectionObserver 对象中止监听工做;
  • takeRecords:为全部监听目标返回一个 IntersectionObserverEntry 对象数组而且中止监听这些目标。

  如下为实现代码:

function LazyLoad (images, options = {}) {
  if (!(this instanceof LazyLoad)) {
    return new LazyLoad(images, options)
  }
  this.setting = Object.assign({}, { src: 'data-src', srcset: 'data-srcset', selector: '.lazyload' }, options)
  this.images = images || document.querySelectorAll(this.setting.selector)
  this.observer = null
  this.init()
}

LazyLoad.prototype.init = function () {
  let self = this
  let observerConfig = {
    root: null,
    rootMargin: '0px',
    threshold: [0]
  }
  this.observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      const target = entry.target
      if (entry.intersectionRatio > 0) {
        this.observer.unobserve(target)
        const src = target.getAttribute(this.setting.src)
        const srcset = target.getAttribute(this.setting.srcset)
        if ('img' === target.tagName.toLowerCase()) {
          if (src) {
            target.src = src
          }
          if (srcset) {
            target.srcset = srcset
          }
        } else {
          target.style.backgroundImage = `url(${src})`
        }
      }
    })
  }, observerConfig)

  this.images.forEach(image => this.observer.observe(image))
}
复制代码

5、总结

  到此,实现图片懒加载主要有两种方法:

  • 监听 scroll 事件,经过 getBoundingClientRect() 计算目标元素与视口的交叉状态;
  • IntersectionObserver 接口。

  第二种方法更加省心,到目前为止兼容性也还行(这一句描述不太恰当,若是想愉快地使用该API,能够采用polyfill的方式w3c IntersectionObserver polyfill,感谢 juejin周辉 指正):

  相比较下,第一种方法须要从各个方面去优化 scroll 事件,从而达到页面滚动“如丝般顺滑”的效果。

  最后,放上两篇关于滚动优化的文章。

相关文章
相关标签/搜索