圖片懶加載的前世此生

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 事件,從而達到頁面滾動「如絲般順滑」的效果。

  最後,放上兩篇關於滾動優化的文章。

相關文章
相關標籤/搜索