小宋爱睡觉 小宋爱睡觉
首页
  • HTML
  • CSS
  • JavaScript
  • Vue
  • React
  • 计算机网络
  • 浏览器原理
  • 性能优化
  • 设计模式
手写系列
  • 字符串
  • 数组
  • 链表
  • 树
  • 动态规划
  • 排序算法
  • GitHub (opens new window)
  • JueJin (opens new window)
首页
  • HTML
  • CSS
  • JavaScript
  • Vue
  • React
  • 计算机网络
  • 浏览器原理
  • 性能优化
  • 设计模式
手写系列
  • 字符串
  • 数组
  • 链表
  • 树
  • 动态规划
  • 排序算法
  • GitHub (opens new window)
  • JueJin (opens new window)
  • 图片压缩
  • 图片懒加载
    • 原理
    • 利用scroll事件
      • getBoundingClientRect()方法
    • 利用IntersectionObserver
  • 渲染十万条数据
  • 如果让你来设计网络
  • articles
Crucials
2022-02-09

图片懒加载

# 图片的懒加载

# 原理

  • 图片懒加载技术主要通过监听图片资源容器是否出现在视口区域内,来决定图片资源是否被加载。

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

# 利用scroll事件

思路

  • 给目标元素指定一张占位图,将真实的图片链接存储在自定义属性中,通常是data-src;
  • 监听与用户滚动行为相关的 scroll 事件;
  • 在 scroll 事件处理程序中利用 Element.getBoundingClientRect() 方法判断目标元素与视口的交叉状态;
  • 当目标元素与视口的交叉状态大于0时,将真实的图片链接赋给目标元素 src 属性或者 backgroundImage 属性。
  • 利用函数节流降低回流
let imgs = document.getElementsByTagName("img"),
  count = 0;
// 首次加载
lazyLoad();
// 通过监听 scroll 事件来判断图片是否到达视口,别忘了防抖节流
window.addEventListener("scroll", throttle(lazyLoad, 160));
function lazyLoad() {
  let viewHeight = document.documentElement.window.innerHeight; //视口高度
  //滚动条卷去的高度
  let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  for (let i = count; i < imgs.length; i++) {
    // 元素现在已经出现在视口中
    if (imgs[i].offsetTop < scrollTop + viewHeight) {
      if (imgs[i].getAttribute("src") !== "default.jpg") continue;
      imgs[i].src = imgs[i].getAttribute("data-src");
      count++;
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 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
  )
}
1
2
3
4
5
6
7
8
9
10
11
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;
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

简单版

function lazyload() {
  for(let i = count; i <num; i++) {
    // 元素现在已经出现在视口中
    if(img[i].getBoundingClientRect().top < document.documentElement.clientHeight) {
      if(img[i].getAttribute("src") !== "default.jpg") continue;
      img[i].src = img[i].getAttribute("data-src");
      count ++;
    }
  }
}
1
2
3
4
5
6
7
8
9
10

# 利用IntersectionObserver

可以异步监听目标元素与其祖先或视窗的交叉状态,注意这个接口是异步的,它不随着目标元素的滚动同步触发,所以它并不会影响页面的滚动性能

options:

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

  • 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))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 或者简单一点
let imgs = document.getElementsByTagName("img")
const observer = new IntersectionObserver(changes => {
    for(let i=0, len=imgs.length; i<len; i++) {
        let img = imgs[i]
        // 通过这个属性判断是否在视口中,返回 boolean 值
        if(img.isIntersecting) {
            const imgElement = img.target
            imgElement.src = imgElement.getAttribute("data-src")
            observer.unobserve(imgElement) // 解除观察
        }
    }
})
Array.from(imgs).forEach(item => observer.observe(item)) // 调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上次更新: 2022/03/10, 14:51:31
图片压缩
渲染十万条数据

← 图片压缩 渲染十万条数据→

Copyright © 2021-2025 粤ICP备2021165371号
  • 跟随系统
  • 浅色模式
  • 深色模式