【硬核】如何写一个自己的图片懒加载函数

当今的 web 页面

回顾一下浏览器从0开始加载网页的过程

现在访问一个 web 页面一般会经过这样的简易步骤:

  1. 用户在浏览器中输入 web 页面对应的域名
  2. 域名经过 DNS 服务器解析成对应的 IP 地址(如果是合法的话)
  3. 客户端与服务端通过三次握手建立 TCP 连接
  4. 浏览器(客户端)向服务器发起 HTTP 请求一个静态的 HTML 页面(e.g index.html,detail.html, home.html ....)
  5. 加载 css (不阻塞) 和 js (一般情况下阻塞) 文件
  6. 遇到一些需要加载资源的标签,比如imgvideoaudio这些标签时,它们会再次向服务器发送 HTTP 请求获取这些标签所需要的资源文件。
  7. ...
  8. 在最后时刻,通过四次挥手断开 TCP 连接

单服务器处理用户访问可能存在的问题

在浏览器从0开始加载网页的过程的第6步,如果当前页面对应的业务存在许多的资源,比如:

  • 电商网站(淘宝,京东)
  • 视频网站(B站、油管、抖音)
  • 新闻网站(今日头条、网易新闻)
  • ...

如果在单个服务器上存在大量的访问并且同时加载页面,必定会增加单个服务器的负担导致服务响应较慢甚至超时。

简单地实现服务器的分流

于是针对上述情况,现在很多网页都是这样处理的:

  1. 把所有的静态 HTML 页面放在自己的服务器上面
  2. 把脚本文件(css、js)放在带宽较高的托管资源服务器上面(e.g 七牛云、阿里云 ...)
  3. 把资源文件(图片、音频、视频)或单个、或切片的放在带宽较高的托管资源服务器上面(e.g 七牛云、阿里云 ...)

百度举例:

  • 请求 HTML
  • 请求脚本文件


  • 请求资源文件

那既然是这样,为什么还要图片优化呢?

服务器分流只是在服务器端的流量分配上面优化了服务器的带宽,但是它并不能解决用户在客户端(e.g 电脑,手机)上面可能还会存在的问题:

  • 网络问题
    1. 网络巨卡,资源加载不出来,导致白屏
    2. 网络很卡,资源能加载但是加载缓慢,存在卡顿
    3. 网络小卡,资源能加载一部分,但是另外一部分白屏或者卡屏
  • 用户体验问题
    1. 图片加载太慢,卡顿或者白屏
    2. 图片全部正常加载并直接显示,用户感知力不强

解决客户端可能存在问题的方式:

  1. 图片的懒加载
  2. 图片的预加载
  3. 显示前的骨架屏
  4. 雪碧图

本文着重讲述如何使用图片懒加载技术完成多图片网页的加载显示。

首先,什么是图片懒加载?

懒加载,顾名思义:相较于普通加载,它可以:

  1. 先统一加载一张 loading 图片(只加载一张图片,而不是全部一次性加载)
  2. 在图片滚动到可视区域范围内,把真正需要的图片地址替换掉之前加载的 loading 图片地址, 并且显示出来。

总体来说,懒加载以总体多加载一张 loading 图片的代价,让其他的图片能够按照特定的条件和时机进行加载,给予了用户一定的缓冲时间。对于一些多图的网站来讲,懒加载是一种不错的选择方案。

懒加载的整体思路:

懒加载的整体步骤如下:

  1. 列表渲染所有的图片,把图片对应的 <img> 标签的 src 属性设置为 loading 图片的地址
html 复制代码
<div class="img-list J_ImgList">
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/4.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" alt="" />
  </div>
</div>
  1. 在图片对应的 <img> 标签设置一个自定义的属性 data-src, 它存放的值为真正需要加载的图片地址的值。
html 复制代码
<div class="img-list J_ImgList">
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/1.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/2.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/3.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/4.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/5.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/6.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/7.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/8.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/9.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/10.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/11.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/12.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/13.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/14.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/15.jpg" alt="" />
  </div>
  <div class="img-list-item">
    <img src="imgs/loading.png" data-src="imgs/16.jpg" alt="" />
  </div>
</div>
  1. 写一下样式
css 复制代码
html,
body {
  margin: 0;
  font-size: 15px;
  color: #3d3d3d;
}

img {
  display: block;
  width: 100%;
  height: 100%;
}

.img-list {
  width: 100%;
}

.img-list::after {
  content: "";
  display: table;
  clear: both;
}

.img-list .img-list-item {
  width: 25%;
  float: left;
  height: 500px;
  padding: 5px;
  box-sizing: border-box;
}
  1. 写一个 IIFE 演示模块
javascript 复制代码
;(function () {
  var init = function () {
    bindEvent();
  }

  function bindEvent() {}

  init();
})();
  1. 获取这些图片元素,绑定事件处理函数 handleScroll
javascript 复制代码
;(function (doc) {
  var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
  var oImgItems = oImgWrap.getElementsByClassName('img-list-item');
  
  var init = function () {
    bindEvent();
  }

  function bindEvent() {
    window.addEventListener('scroll', handleScroll, false);
  }

  function handleScroll() {
    // do something
  }

  init();
})(document);
  1. 封装一个函数 lazyLoadImg() 判断这些图片是否处于可视范围之内,如果处于可视范围之内并且存在 data-src属性,使用 data-src替换掉原来的 src的值,删除 data-src
javascript 复制代码
/**
 * @function lazyLoadImg
 * @description 懒加载图片元素
 * @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
 * @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
 * @param {number} clientHeight 容器的整体高度
 * @param {number} scrollTop 当前容器已滚动的高度
 * @return {void} 没有返回值
 */
function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
  for (var i = 0; i < oImgItems.length; i++) {
    var oImgItem = oImgItems[i];
    var oImg = oImgItem.getElementsByTagName('img')[0];
    var offsetTop = oImgItem.offsetTop;
    
    if (clientHeight + scrollTop >= offsetTop) {
      if (!!oImg.getAttribute('data-src')) {
        oImg.setAttribute('src', oImg.getAttribute('data-src'));
        oImg.removeAttribute('data-src');
      }
    }
  }
}
  1. 设置lazyLoadImg()在 init 时执行; 在 handleScroll 的时候,也执行一下 lazyLoadImg()
javascript 复制代码
;(function (doc) {
  var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
  var oImgItems = oImgWrap.getElementsByClassName('img-list-item');

  var init = function () {
    lazyLoadImg(
      oImgItems,
      document.documentElement.clientHeight,
      document.documentElement.scrollTop || document.body.scrollTop
    );
    bindEvent();
  }

  function bindEvent() {
    window.addEventListener('scroll', utils.throttle(handleScroll), false);
  }

  function handleScroll(e) {
    var clientHeight = document.documentElement.clientHeight;
    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

    lazyLoadImg(oImgItems, clientHeight, scrollTop);
  }

  /**
   * @function lazyLoadImg
   * @description 懒加载图片元素
   * @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
   * @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
   * @param {number} clientHeight 容器的整体高度
   * @param {number} scrollTop 当前容器已滚动的高度
   * @return {void} 没有返回值
   */
  function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
    for (var i = 0; i < oImgItems.length; i++) {
      var oImgItem = oImgItems[i];
      var oImg = oImgItem.getElementsByTagName('img')[0];
      var offsetTop = oImgItem.offsetTop;
      
      if (clientHeight + scrollTop >= offsetTop) {
        if (!!oImg.getAttribute('data-src')) {
          oImg.setAttribute('src', oImg.getAttribute('data-src'));
          oImg.removeAttribute('data-src');
        }
      }
    }
  }

  init();
})(document);
  1. 在 init 时执行时, 需要将页面滚动到顶部。
javascript 复制代码
;(function (doc) {
  var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
  var oImgItems = oImgWrap.getElementsByClassName('img-list-item');

  var init = function () {
    window.scroll(0, 0);
    lazyLoadImg(
      oImgItems,
      document.documentElement.clientHeight,
      document.documentElement.scrollTop || document.body.scrollTop
    );
    bindEvent();
  }

  function bindEvent() {
    window.addEventListener('scroll', utils.throttle(handleScroll), false);
  }

  function handleScroll(e) {
    var clientHeight = document.documentElement.clientHeight;
    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

    lazyLoadImg(oImgItems, clientHeight, scrollTop);
  }

  /**
   * @function lazyLoadImg
   * @description 懒加载图片元素
   * @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
   * @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
   * @param {number} clientHeight 容器的整体高度
   * @param {number} scrollTop 当前容器已滚动的高度
   * @return {void} 没有返回值
   */
  function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
    for (var i = 0; i < oImgItems.length; i++) {
      var oImgItem = oImgItems[i];
      var oImg = oImgItem.getElementsByTagName('img')[0];
      var offsetTop = oImgItem.offsetTop;
      
      if (clientHeight + scrollTop >= offsetTop) {
        if (!!oImg.getAttribute('data-src')) {
          oImg.setAttribute('src', oImg.getAttribute('data-src'));
          oImg.removeAttribute('data-src');
        }
      }
    }
  }

  init();
})(document);

代码优化:

优化点一:

滚动触发太频繁,使用节流函数控制单位时间内触发滚动的频率:

javascript 复制代码
;(function (doc) {
  var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
  var oImgItems = oImgWrap.getElementsByClassName('img-list-item');

  var init = function () {
    bindEvent();
  }

  function bindEvent() {
    window.scroll(0, 0);
    lazyLoadImg(
      oImgItems,
      document.documentElement.clientHeight,
      document.documentElement.scrollTop || document.body.scrollTop
    );
    window.addEventListener('scroll', throttle(handleScroll, 300), false);
  }

  function handleScroll(e) {
    var clientHeight = document.documentElement.clientHeight;
    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

    lazyLoadImg(oImgItems, clientHeight, scrollTop);
  }

  /**
   * @function lazyLoadImg
   * @description 懒加载图片元素
   * @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
   * @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
   * @param {number} clientHeight 容器的整体高度
   * @param {number} scrollTop 当前容器已滚动的高度
   * @return {void} 没有返回值
   */
  function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
    for (var i = 0; i < oImgItems.length; i++) {
      var oImgItem = oImgItems[i];
      var oImg = oImgItem.getElementsByTagName('img')[0];
      var offsetTop = oImgItem.offsetTop;
      
      if (clientHeight + scrollTop >= offsetTop) {
        if (!!oImg.getAttribute('data-src')) {
          oImg.setAttribute('src', oImg.getAttribute('data-src'));
          oImg.removeAttribute('data-src');
        }
      }
    }
  }

  init();
})(document);
javascript 复制代码
/**
 * @function throttle
 * @description 节流函数
 * @param {Function} fn 需要被节流执行的函数
 * @param {number | undefined} delay 需要被节流的时长 (默认是 300 ms)
 * @return {Function} 返回一个被节流的执行函数
 */
function throttle(fn, delay) {
  var _delay = typeof delay === 'number' && delay > 0 ? delay : 300;
  var timer = null;
  var startTime = Date.now();

  return function () {
    var args = arguments;
    var ctx = this;
    var endTime = Date.now();

    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    if (endTime - startTime >= _delay) {
      startTime = endTime;
    } else {
      timer = setTimeout(function () {
        fn.apply(ctx, args);
        startTime = Date.now();
        clearTimeout(timer);
        timer = null;
      }, endTime - startTime);
    }
  }
}

优化点二:

如果图片滚动到最底部,图片列表的懒加载就会失效。

这里需要在 init 的时候就把窗口滚动到最顶部,并且其他的程序延迟执行。

javascript 复制代码
;(function (doc) {
  var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
  var oImgItems = oImgWrap.getElementsByClassName('img-list-item');

  var init = function () {
    var t = setTimeout(function() {
      window.scrollTo(0, 0);
      lazyLoadImg(
        oImgItems,
        document.documentElement.clientHeight,
        document.documentElement.scrollTop || document.body.scrollTop
      );
      bindEvent();

      clearTimeout(t);
      t = null;
    }, 300);
  }

  function bindEvent() {
    window.addEventListener('scroll', utils.throttle(handleScroll), false);
  }

  function handleScroll(e) {
    console.log('handleScroll', e);
    var clientHeight = document.documentElement.clientHeight;
    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

    lazyLoadImg(oImgItems, clientHeight, scrollTop);
  }

  /**
   * @function lazyLoadImg
   * @description 懒加载图片元素
   * @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
   * @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
   * @param {number} clientHeight 容器的整体高度
   * @param {number} scrollTop 当前容器已滚动的高度
   * @return {void} 没有返回值
   */
  function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
    for (var i = 0; i < oImgItems.length; i++) {
      var oImgItem = oImgItems[i];
      var oImg = oImgItem.getElementsByTagName('img')[0];
      var offsetTop = oImgItem.offsetTop;
      
      if (clientHeight + scrollTop >= offsetTop) {
        if (!!oImg.getAttribute('data-src')) {
          oImg.setAttribute('src', oImg.getAttribute('data-src'));
          oImg.removeAttribute('data-src');
        }
      }
    }
  }

  init();
})(document);

优化点三:

lazyLoadImg需要作为一个纯函数,把它提取到工具外面,然后将工具导入到模块中使用

javascript 复制代码
var utils = (function () {
  /**
   * @function lazyLoadImg
   * @description 懒加载图片元素
   * @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
   * @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
   * @param {number} clientHeight 容器的整体高度
   * @param {number} scrollTop 当前容器已滚动的高度
   * @return {void} 没有返回值
   */
  function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
    for (var i = 0; i < oImgItems.length; i++) {
      var oImgItem = oImgItems[i];
      var oImg = oImgItem.getElementsByTagName('img')[0];
      var offsetTop = oImgItem.offsetTop;
      
      if (clientHeight + scrollTop >= offsetTop) {
        if (!!oImg.getAttribute('data-src')) {
          oImg.setAttribute('src', oImg.getAttribute('data-src'));
          oImg.removeAttribute('data-src');
        }
      }
    }
  }

  /**
   * @function throttle
   * @description 节流函数
   * @param {Function} fn 需要被节流执行的函数
   * @param {number | undefined} delay 需要被节流的时长 (默认是 300 ms)
   * @return {Function} 返回一个被节流的执行函数
   */
  function throttle(fn, delay) {
    var _delay = typeof delay === 'number' && delay > 0 ? delay : 300;
    var timer = null;
    var startTime = Date.now();

    return function () {
      var args = arguments;
      var ctx = this;
      var endTime = Date.now();

      if (timer) {
        clearTimeout(timer);
        timer = null;
      }

      if (endTime - startTime >= _delay) {
        startTime = endTime;
      } else {
        timer = setTimeout(function () {
          fn.apply(ctx, args);
          startTime = Date.now();
          clearTimeout(timer);
          timer = null;
        }, endTime - startTime);
      }
    }
  }

  return {
    lazyLoadImg: lazyLoadImg,
    throttle: throttle,
  };
})();
javascript 复制代码
;(function (doc, utils) {
  var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
  var oImgItems = oImgWrap.getElementsByClassName('img-list-item');

  var init = function () {
    var t = setTimeout(function() {
      window.scrollTo(0, 0);
      utils.lazyLoadImg(
        oImgItems,
        document.documentElement.clientHeight,
        document.documentElement.scrollTop || document.body.scrollTop
      );
      bindEvent();

      clearTimeout(t);
      t = null;
    }, 300);
  }

  function bindEvent() {
    window.addEventListener('scroll', utils.throttle(handleScroll), false);
  }

  function handleScroll(e) {
    console.log('handleScroll', e);
    var clientHeight = document.documentElement.clientHeight;
    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

    utils.lazyLoadImg(oImgItems, clientHeight, scrollTop);
  }

  init();
})(document, utils);

整体代码实现:

img-lazyload-playground.zip

相关推荐
_.Switch40 分钟前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光44 分钟前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   44 分钟前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   1 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web1 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇2 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr2 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho3 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常4 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js