自定义一个图片懒加载指令

383 阅读2分钟

什么是懒加载

懒加载是针对图片加载时机的优化,适用于图片量比较大的使用场景,当页面初始化时一次性加载所有图片容易造成白屏、卡顿的现象,懒加载的实现就是先加载可视区域的图片,不可见的图片等变为可见时才开始加载。

实现一个懒加载指令

了解了懒加载,我们来自己写一个自定义的懒加载指令吧,目标是实现元素可见自动加载img或者元素背景图

实现思路

  1. dom绑定指令时,将指令绑定的src值设置到元素的data-src属性上
  2. 浏览器支持IntersectionObserver,支持则生成观察者实例,观察dom是否可见
  3. 浏览器不支持IntersectionObserver,监听scroll事件,根据元素的位置判断dom是否可见
  4. 元素可见时,如果是img元素设置元素的真实src, 其他元素则设置真实的backgroundImage, 删除data-src属性,取消对元素可见性的观察,取消scroll事件的监听
  5. 元素绑定的src更新时,判断dom 是否有data-src属性,有则没有加载过,直接替换data-src为新的src值,没有则判断元素是否可见,元素可见加载新的src,不可见则设置data-src为新的src值,重新观察dom可见性
  6. 元素unbind时,取消可见线观察,取消事件监听防止内存泄漏

lazy.ts


import { throttle } from 'g/utils'

/**
 * Usage:
 *
 * In template:
 *
 * // not image tag
 * <div v-lazy="url"></div>
 * // image tag
 * <img v-lazy="src">
 */

// 保存观察dome可见性的观察器实例,方便图片加载后取消对dom是否可见的观察
let ob: any = null
// 保存scroll事件的回调,方便图片加载后取消监听
const handleStore: any = new WeakMap()

const lazyDirective = {
  bind(el, binding) {
    const src = binding.value
    if (!src || typeof src !== 'string') return
    el.setAttribute('data-src', src)
  },
  inserted(el) {
    observeEl(el)
  },
  update(el, binding) {
    if (binding.value === binding.oldValue) return
    const src = el.getAttribute('data-src')
    if (src) {
      // 未加载过的图片src变化,直接修改data-src属性
      el.setAttribute('data-src', binding.value)
    } else {
      // 加载过的图片src变化,判断是否可见,dom可见加载新的src对应图片,dom不可见修改data-src属性,dom设置可见性观察
      if (isVisible(el)) {
        if (el.tagName === 'IMG') {
          el.setAttribute('src', binding.value)
        } else {
          el.style.backgroundImage = `url(${binding.value})`
        }
      } else {
        el.setAttribute('data-src', binding.value)
        observeEl(el)
      }
    }
  },
  unbind(el) {
    unobserve(el)
  }
}
const observeEl = (el: HTMLElement) => {
  if ('IntersectionObserver' in window) {
    if (!ob) {
      ob = new IntersectionObserver(entries => {
        entries.forEach(entry => {
          // 元素可见
          if (entry.isIntersecting) {
            loadImage(entry.target as HTMLElement)
          }
        })
      })
    }
    ob.observe(el)
  } else {
    lazyLoad(el)
    // 滚动事件节流,避免频繁触发
    const handle: any = throttle(lazyLoad, 200)
    const handleCallback = () => {
      handle(el)
    }
    handleStore.set(el, handleCallback)
    window.addEventListener('wheel', handleCallback)
  }
}
const isVisible = (el: HTMLElement) => {
  const windowHeight = window.innerHeight
  const { top, bottom } = el.getBoundingClientRect()
  return top - windowHeight < 0 && bottom > 0
}
const lazyLoad = (el: HTMLElement) => {
  if (isVisible(el)) {
    loadImage(el)
  }
}
const loadImage = (el: HTMLElement) => {
  const src = el.getAttribute('data-src')
  if (!src) return
  if (el.tagName === 'IMG') {
    // 加载真实的src
    el.setAttribute('src', src)
  } else {
    // 加载真实的backgroundImage
    el.style.backgroundImage = `url(${src})`
  }
  el.removeAttribute('data-src')
  unobserve(el)
}
const unobserve = (el: HTMLElement) => {
  if (ob) {
    // 取消观察el的可见性
    ob.unobserve(el)
  } else { 
    // 取消事件订阅
    const handleCallback = handleStore.get(el)
    if (handleCallback) {
      window.removeEventListener('wheel', handleCallback)
      handleStore.delete(el)
    }
  }
}

export default lazyDirective

utils.ts

export const throttle = (
  func: (...args: any[]) => any,
  wait: number,
  immediate: boolean = false
) => {
  let timeout: number | undefined
  return function(this: any) {
    const context = this
    const args = arguments
    const later = () => {
      timeout = void 0
      if (!immediate) {
        // @ts-ignore
        func.apply(context, args)
      }
    }

    if (immediate && !timeout) {
      func.apply(context, args)
    }

    if (!timeout) {
      timeout = window.setTimeout(later, wait)
    }
  }
}

使用

import lazy from 'g/directive/lazy'

Vue.directive('lazy', lazy)

<img v-lazy="src">

<div v-lazy="url"></div>