原生 JS 实现最简单的图片懒加载

懒加载

什么是懒加载

懒加载其实就是延迟加载,是一种对网页性能优化的方式,好比当访问一个页面的时候,优先显示可视区域的图片而不一次性加载全部图片,当须要显示的时候再发送图片请求,避免打开网页时加载过多资源。html

何时用懒加载

当页面中须要一次性载入不少图片的时候,每每都是须要用懒加载的。java

懒加载原理

咱们都知道HTML中的 <img>标签是表明文档中的一个图像。。说了个废话。。git

<img>标签有一个属性是 src,用来表示图像的URL,当这个属性的值不为空时,浏览器就会根据这个值发送请求。若是没有 src属性,就不会发送请求。github

嗯?貌似这点能够利用一下?api

我先不设置 src,须要的时候再设置?数组

nice,就是这样。浏览器

咱们先不给 <img>设置 src,把图片真正的URL放在另外一个属性 data-src中,在须要的时候也就是图片进入可视区域的以前,将URL取出放到 src中。性能优化

实现


HTML结构

  1. <div class="container">app

  2.  <div class="img-area">函数

  3.    <img class="my-photo" alt="loading" src="./img/img1.png">

  4.  </div>

  5.  <div class="img-area">

  6.    <img class="my-photo" alt="loading" src="./img/img2.png">

  7.  </div>

  8.  <div class="img-area">

  9.    <img class="my-photo" alt="loading" src="./img/img3.png">

  10.  </div>

  11.  <div class="img-area">

  12.    <img class="my-photo" alt="loading" src="./img/img4.png">

  13.  </div>

  14.  <div class="img-area">

  15.    <img class="my-photo" alt="loading" src="./img/img5.png">

  16.  </div>

  17. </div>

仔细观察一下, <img>标签此时是没有 src属性的,只有 altdata-src属性。

alt 属性是一个必需的属性,它规定在图像没法显示时的替代文本。 data-* 全局属性:构成一类名称为自定义数据属性的属性,能够经过 HTMLElement.dataset来访问。

如何判断元素是否在可视区域

方法一

网上看到好多这种方法,稍微记录一下。

  1. 经过 document.documentElement.clientHeight获取屏幕可视窗口高度

  2. 经过 document.documentElement.scrollTop获取浏览器窗口顶部与文档顶部之间的距离,也就是滚动条滚动的距离

  3. 经过 element.offsetTop获取元素相对于文档顶部的距离

而后判断②-③<①是否成立,若是成立,元素就在可视区域内。

方法二(推荐)

经过 getBoundingClientRect()方法来获取元素的大小以及位置,MDN上是这样描述的:

The Element.getBoundingClientRect() method returns the size of an element and its position relative to the viewport.

这个方法返回一个名为 ClientRectDOMRect对象,包含了 toprightbottonleftwidthheight这些值。

MDN上有这样一张图:

 

能够看出返回的元素位置是相对于左上角而言的,而不是边距。

咱们思考一下,什么状况下图片进入可视区域。

假设 constbound=el.getBoundingClientRect();来表示图片到可视区域顶部距离; 并设 constclientHeight=window.innerHeight;来表示可视区域的高度。

随着滚动条的向下滚动, bound.top会愈来愈小,也就是图片到可视区域顶部的距离愈来愈小,当 bound.top===clientHeight时,图片的上沿应该是位于可视区域下沿的位置的临界点,再滚动一点点,图片就会进入可视区域。

也就是说,在 bound.top<=clientHeight时,图片是在可视区域内的。

咱们这样判断:

  1. function isInSight(el) {

  2.  const bound = el.getBoundingClientRect();

  3.  const clientHeight = window.innerHeight;

  4.  //若是只考虑向下滚动加载

  5.  //const clientWidth = window.innerWeight;

  6.  return bound.top <= clientHeight + 100;

  7. }

这里有个+100是为了提早加载。

加载图片

页面打开时须要对全部图片进行检查,是否在可视区域内,若是是就加载。

  1. function checkImgs() {

  2.  const imgs = document.querySelectorAll('.my-photo');

  3.  Array.from(imgs).forEach(el => {

  4.    if (isInSight(el)) {

  5.      loadImg(el);

  6.    }

  7.  })

  8. }

  9.  

  10. function loadImg(el) {

  11.  if (!el.src) {

  12.    const source = el.dataset.src;

  13.    el.src = source;

  14.  }

  15. }

这里应该是有一个优化的地方,设一个标识符标识已经加载图片的index,当滚动条滚动时就不须要遍历全部的图片,只须要遍历未加载的图片便可。

函数节流

在相似于滚动条滚动等频繁的DOM操做时,总会提到“函数节流、函数去抖”。

所谓的函数节流,也就是让一个函数不要执行的太频繁,减小一些过快的调用来节流。

基本步骤:

  1. 获取第一次触发事件的时间戳

  2. 获取第二次触发事件的时间戳

  3. 时间差若是大于某个阈值就执行事件,而后重置第一个时间

  1. function throttle(fn, mustRun = 500) {

  2.  const timer = null;

  3.  let previous = null;

  4.  return function() {

  5.    const now = new Date();

  6.    const context = this;

  7.    const args = arguments;

  8.    if (!previous){

  9.      previous = now;

  10.    }

  11.    const remaining = now - previous;

  12.    if (mustRun && remaining >= mustRun) {

  13.      fn.apply(context, args);

  14.      previous = now;

  15.    }

  16.  }

  17. }

这里的 mustRun就是调用函数的时间间隔,不管多么频繁的调用 fn,只有 remaining>=mustRunfn才能被执行。

实验


页面打开时

 

能够看出此时仅仅是加载了img1和img2,其它的img都没发送请求,看看此时的浏览器

 

第一张图片是完整的呈现了,第二张图片刚进入可视区域,后面的就看不到了~

页面滚动时

当我向下滚动,此时浏览器是这样

 

此时第二张图片彻底显示了,而第三张图片显示了一点点,这时候咱们看看请求状况

 

img3的请求发出来,然后面的请求仍是没发出~

所有载入时

当滚动条滚到最底下时,所有请求都应该是发出的,如图

 

更新


方法三 IntersectionObserver

经大佬提醒,发现了这个方法

先附上连接:

jjc大大:

https://github.com/justjavac/the-front-end-knowledge-you-may-dont-know/issues/10

阮一峰大大:

http://www.ruanyifeng.com/blog/2016/11/intersectionobserver_api.html

API Sketch for Intersection Observers:

https://github.com/WICG/IntersectionObserver

IntersectionObserver能够自动观察元素是否在视口内。

  1. var io = new IntersectionObserver(callback, option);

  2. // 开始观察

  3. io.observe(document.getElementById('example'));

  4. // 中止观察

  5. io.unobserve(element);

  6. // 关闭观察器

  7. io.disconnect();

callback的参数是一个数组,每一个数组都是一个 IntersectionObserverEntry对象,包括如下属性:

属性 描述
time 可见性发生变化的时间,单位为毫秒
rootBounds 与getBoundingClientRect()方法的返回值同样
boundingClientRect 目标元素的矩形区域的信息
intersectionRect 目标元素与视口(或根元素)的交叉区域的信息
intersectionRatio 目标元素的可见比例,即intersectionRect占boundingClientRect的比例,彻底可见时为1,彻底不可见时小于等于0
target 被观察的目标元素,是一个 DOM 节点对象

咱们须要用到 intersectionRatio来判断是否在可视区域内,当 intersectionRatio>0&&intersectionRatio<=1即在可视区域内。

代码

  1. function checkImgs() {

  2.  const imgs = Array.from(document.querySelectorAll(".my-photo"));

  3.  imgs.forEach(item => io.observe(item));

  4. }

  5.  

  6. function loadImg(el) {

  7.  if (!el.src) {

  8.    const source = el.dataset.src;

  9.    el.src = source;

  10.  }

  11. }

  12.  

  13. const io = new IntersectionObserver(ioes => {

  14.  ioes.forEach(ioe => {

  15.    const el = ioe.target;

  16.    const intersectionRatio = ioe.intersectionRatio;

  17.    if (intersectionRatio > 0 && intersectionRatio <= 1) {

  18.      loadImg(el);

  19.    }

  20.    el.onload = el.onerror = () => io.unobserve(el);

  21.  });

  22. });

相关文章
相关标签/搜索