点击勘误issues (opens new window),哪吒感谢大家的阅读

懒加载,顾名思义,在当前网页,滑动页面到能看到图片的时候再加载图片

故问题拆分成两个:

  1. 如何判断图片出现在了当前视口 (即如何判断我们能够看到图片)
  2. 如何控制图片的加载

# 方案一: 位置计算 + 滚动事件 (Scroll) + DataSet API

# 如何判断图片出现在了当前视口

clientTop,offsetTop,clientHeight 以及 scrollTop 各种关于图片的高度作比对

仅仅知道它静态的高度还不够,我们还需要知道动态的

如何动态?监听 window.scroll 事件

# 如何控制图片的加载

<img data-src="xxx.jpg" />
1

首先设置一个临时 Data 属性 data-src,控制加载时使用 src 代替 data-src,可利用 DataSet API 实现

img.src = img.dataset.src
1

# 方案二: getBoundingClientRect API + Scroll with Throttle + DataSet API

引入一个新的 API, Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。

那如何判断图片出现在了当前视口呢,根据示例图示意,代码如下

// clientHeight 代表当前视口的高度
img.getBoundingClientRect().top < document.documentElement.clientHeight;
1
2

监听 window.scroll 事件也优化一下

加个节流器,提高性能。工作中一般使用 lodash.throttle 就可以了,万能的 lodash 啊!

_.throttle(func, [(wait = 0)], [(options = {})]);
1
  • 防抖:防止抖动,单位时间内事件触发会被重置,避免事件被误伤触发多次。代码实现重在清零 clearTimeout。防抖可以比作等电梯,只要有一个人进来,就需要再等一会儿。业务场景有避免登录按钮多次点击的重复提交。
  • 节流:控制流量,单位时间内事件只能触发一次,与服务器端的限流 (Rate Limit) 类似。代码实现重在开锁关锁 timer=timeout; timer=null。节流可以比作过红绿灯,每等一个红灯时间就可以过一批。

# 防抖 (debounce)

防抖,顾名思义,防止抖动,以免把一次事件误认为多次,敲键盘就是一个每天都会接触到的防抖操作。

想要了解一个概念,必先了解概念所应用的场景。在 JS 这个世界中,有哪些防抖的场景呢

  1. 登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖
  2. 调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
  3. 文本编辑器实时保存,当无任何更改操作一秒后进行保存

代码如下,可以看出来防抖重在清零 clearTimeout(timer)

function debounce (f, wait) {
  let timer
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      f(...args)
    }, wait)
  }
}
1
2
3
4
5
6
7
8
9

# 节流 (throttle)

节流,顾名思义,控制水的流量。控制事件发生的频率,如控制为1s发生一次,甚至1分钟发生一次。与服务端(server)及网关(gateway)控制的限流 (Rate Limit) 类似。

  1. scroll 事件,每隔一秒计算一次位置信息等
  2. 浏览器播放事件,每个一秒计算一次进度信息等
  3. input 框实时搜索并发送请求展示下拉列表,每隔一秒发送一次请求 (也可做防抖)

代码如下,可以看出来节流重在加锁 timer=timeout

function throttle (f, wait) {
  let timer
  return (...args) => {
    if (timer) { return }
    timer = setTimeout(() => {
      f(...args)
      timer = null
    }, wait)
  }
}
1
2
3
4
5
6
7
8
9
10

# 方案三: IntersectionObserver API + DataSet API

如何判断图片出现在了当前视口

方案二使用的方法是: window.scroll 监听 Element.getBoundingClientRect() 并使用 _.throttle 节流

一系列组合动作太复杂了,于是浏览器出了一个三合一事件: IntersectionObserver API,一个能够监听元素是否到了当前视口的事件,一步到位!

事件回调的参数是 IntersectionObserverEntry (打开新窗口)的集合,代表关于是否在可见视口的一系列值

其中,entry.isIntersecting 代表目标元素可见

const observer = new IntersectionObserver((changes) => {
  // changes: 目标元素集合
  changes.forEach((change) => {
    // intersectionRatio
    if (change.isIntersecting) {
      const img = change.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

observer.observe(img);
1
2
3
4
5
6
7
8
9
10
11
12
13

当然,IntersectionObserver 除了给图片做懒加载外,还可以对单页应用资源做预加载。

如在 next.js v9 中,会对视口内的资源做预加载,可以参考 next 9 production optimizations(打开新窗口)

<Link href="/about">
  <a>xxx</a>
</Link>
1
2
3

# 方案四: LazyLoading 属性

浏览器觉得懒加载这事可以交给自己做,你们开发者加个属性就好了。实在是...!

<img src="shanyue.jpg" loading="lazy" />
1

不过目前浏览器兼容性不太好,关于 loading 属性的文章也可以查看 Native image lazy-loading for the web!

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>图片懒加载</title>
    <style>
      img {
        width: 100%;
        height: 600px;
      }
    </style>
  </head>
  <body>
    <img
      src="https://cdn.pixabay.com/photo/2021/08/24/15/38/sand-6570980_960_720.jpg"
      alt="1"
    />
    <img
      src="https://cdn.pixabay.com/photo/2013/02/21/19/06/drink-84533_960_720.jpg"
      alt="2"
    />
    <img
      data-src="https://cdn.pixabay.com/photo/2014/12/15/17/16/boardwalk-569314_960_720.jpg"
      alt="3"
    />
    <img
      data-src="https://cdn.pixabay.com/photo/2013/07/18/20/26/sea-164989_960_720.jpg"
      alt="4"
    />
    <img
      data-src="https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885_960_720.jpg"
      alt="5"
    />
    <img
      data-src="https://cdn.pixabay.com/photo/2017/03/26/21/54/yoga-2176668_960_720.jpg"
      alt="6"
    />
    <img
      data-src="https://cdn.pixabay.com/photo/2015/03/17/14/05/sparkler-677774_960_720.jpg"
      alt="7"
    />
    <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.20/lodash.js"></script>
    <script>
      const images = document.querySelectorAll("img");
      const lazyLoad = () => {
        images.forEach((item) => {
          // 触发条件为img元素的CSSOM对象到视口顶部的距离 < 100px + 视口高度,+100px为了提前触发图片加载
          if (
            item.getBoundingClientRect().top <
            document.documentElement.clientHeight + 100
          ) {
            if ("src" in item.dataset) {
              item.src = item.dataset.src;
            }
          }
        });
      };
      document.addEventListener("scroll", _.throttle(lazyLoad, 200));
    </script>
  </body>
</html>
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
61
62
上次更新: 1/19/2022, 5:46:58 PM