# 什么是图片懒加载

  • 图片的懒加载就是在页面打开的时候,不要一次性全部显示页面所有的图片,而是只显示当前视口内的图片,一般在移动端使用(PC 端主要是前端分页或者后端分页)。

# 为什么需要懒加载

  • 对于一个页面加载速度影响最大的因素之一就是图片资源,如果一个页面图片太多(比如某宝,某东等),整个页面的图片大小可以到达几百兆,即使在百兆宽带,全部下载的话,也需要上十秒的时间,这对于用户耐心的考验是巨大的,更别说网络差的地方了。
  • 因此,懒加载是必须要做的,对于页面未在可视区域内显示的图片先不做加载处理,只加载第一映入眼帘的图片,由于可视区域显示的图片少,加载速度就会大大提升,用户体验也会更好。
  • 而且,用户可能只翻看一两页就退出了,剩下未查看的图片也就不需要加载了。这也相当于节省了带宽资源。

# 懒加载实现原理

  • 由于浏览器会自动对页面中的 img 标签的 src 属性发送请求并下载图片。因此,通过 html5 自定义属性 data-xxx 先暂存 src 的值,然后在需要显示的时候,再将 data-xxx 的值重新赋值到 img 的 src 属性即可。

# 懒加载思路及实现

  • 实现懒加载有四个步骤,如下:
    • 加载 loading 图片
    • 判断哪些图片要加载【重点】
    • 隐形加载图片
    • 替换真图片
  • 另外这里还涉及到了节流函数与瀑布流布局不懂的可以查看之前发的文章,这里贴一下代码

# 瀑布流代码

window.addEventListener('load', waterFall('#main', '.box'))
// 1.0 定义一个函数 接收 父元素 与 子元素们 选择器
function waterFall(parent, childs) {
  // 1.1 获取标签父元素与其中的所有子元素
  const parentEl = document.querySelector(parent)
  const childsEls = document.querySelectorAll(childs)
  // 1.2 获取一个子元素的宽度
  const childElWidth = childsEls[0].offsetWidth
  // 1.3 获取屏幕的宽度 进行兼容判断
  const screenWidth = document.documentElement.clientWidth || document.body.clientWidth
  // 1.4 根据上面条件计算出应排版列数
  const cols = ~~(screenWidth / childElWidth)
  // 1.5 给父元素宽度并进行居中
  parentEl.style.width = cols * childElWidth + 'px'
  parentEl.style.margin = '0 auto'
  // 2.0 声明数组 存放每次应该定位的高度 数组长度与列数一致
  const childsElHeightArr = []
  // 2.1 声明三个变量 分别用于保存 数组中最小高度、元素高度与最小索引值
  let minChildElHeight = 0, childElHeight = 0, minIndex = 0
  // 3.0 对所有子元素进行遍历
  childsEls.forEach((item, index) => {
    // 3.1 遍历获取元素的高度
    childElHeight = item.offsetHeight
    // 3.2 判断如果列数小于 index 则是第一排元素 添加进数组用于计算高度 
    if (cols > index) return childsElHeightArr.push(childElHeight)
    // 3.3 否则利用算法金星定位
    // 3.4 计算出数组中的最小高度
    minChildElHeight = Math.min(...childsElHeightArr)
    // 3.5 计算数组最小高度所在的索引 这里我封装一个函数 calcMinIndex 即 -> 4.0 
    minIndex = calcMinIndex(childsElHeightArr)
    // 3.6 进行绝对定位
    item.style.position = 'absolute'
    // 3.7 利用最小索引 * 元素宽度 得出子元素据左的距离
    item.style.left = minIndex * childElWidth + 'px'
    // 3.8 将每次最小高度赋值给元素的高度
    item.style.top = minChildElHeight + 'px'
    // 3.9 将每次最小高度值与元素高度进行相加
    childsElHeightArr[minIndex] += childElHeight
    // 4.0 封装计算数组最小值的索引
    function calcMinIndex(array) {
      let index = 0
      for (let i = 0; i < array.length; i++) {
        if (array[index] > array[i]) index = i
      }
      return index
    }
  })
}

# 节流函数

function throttle(fn, interval, options = {
  leading: false,
  trailing: false
}) {
  let lastTriggerTime = 0
  let timer = null
  const {
    leading,
    trailing,
    resultCallback
  } = options
  const _throtle = function (...args) {
    return new Promise((resolve, reject) => {
      const triggerTime = +new Date
      if (!leading && !lastTriggerTime) lastTriggerTime = triggerTime
      const remainTime = interval - (triggerTime - lastTriggerTime)
      if (remainTime <= 0) {
        if (timer) {
          clearTimeout(timer)
          timer = null
        }
        const res = fn.apply(this, args)
        try {
          if (Object.prototype.toString.call(resultCallback) === '[object Function]') resolve(res)
        } catch (err) {
          reject(err)
        }
        return lastTriggerTime = triggerTime
      }
      if (trailing && !timer) {
        timer = setTimeout(() => {
          const res = fn.apply(this, args)
          try {
            if (Object.prototype.toString.call(resultCallback) === '[object Function]') resolve(res)
          } catch (err) {
            reject(err)
          }
          timer = null
          lastTriggerTime = +new Date
        }, remainTime)
      }
    })
  }
  _throtle.cancel = function () {
    if (timer) {
      clearTimeout(timer)
      timer = null
      lastTriggerTime = 0
    }
  }
  return _throtle
}

# 实现 lazyload

  • 不熟悉 getBoundingClientRect 详情 MDN 查看:https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect
function lazyload() {
  // 1. 获取当前文档高度
  const viewHeight = document.documentElement.clientHeight || window.innerHeight
  // 2. 获取文档中所有拥有 data-src lazyload 属性的 & lt;img > 节点 即需要懒加载的图片标签
  let imgs = document.querySelectorAll('img[data-src][lazyload]')
  imgs.forEach((img, i) => {
    // 3.HTMLElement.dataset 属性允许无论是在读取模式和写入模式下访问在 HTML 或 DOM 中的元素上设置的所有自定义数据属性 (data -*) 集。
    if (!img.dataset.src) return // 如果自定义 data-src 为空直接 return
    // 4.dataset 能获取到 data - 开头的自定义属性
    let src = img.dataset.src
    // 5.1 返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects () 方法返回的一组矩形的集合,就是该元素的 CSS 边框大小。
    // 5.2 返回的结果是包含完整元素的最小矩形,并且拥有 left, top, right, bottom, x, y, width, 和 height 这几个以像素为单位的只读属性用于描述整个边框。
    // 5.3 除了 width 和 height 以外的属性是相对于视图窗口的左上角来计算的。
    let { bottom, top } = img.getBoundingClientRect()
    // 6. 其 top 值是相对于当前视窗的顶部而言的而不是绝对的顶部,所以 top 值 < window.innerHeight 的话图片就出现在底部了就需要加载
    if (bottom > 0 && top < viewHeight) {
      // 7.new 了一个 Image 创建 img 标签,img 加载图片路径,触发 onload 事件再给原来的 html 结构中的 img 赋值路径
      const image = new Image()
      image.src = src
      image.onload = img.src = src
      // 8. 移除属性避免重复加载
      img.removeAttribute('data-src')
      img.removeAttribute('lazyload')
    }
  })
}
// 先调用一次加载最初显示在视窗中的图片
window.addEventListener('load', lazyload)
// 节流 leading 先触发一次 trailing 最后一次也会触发
const throttle_lazyload = throttle(lazyload, 1000, {
  leading: true,
  trailing: true,
})
// 箭头 scroll (滚动) 事件
window.addEventListener('scroll', () => {
  throttle_lazyload()
  waterFall('#main', '.box', main) // 瀑布流
})
  • 至此,实现图片懒加载篇章也告辞段落了,完成了之前瀑布流文章中许下的承诺了~