源码地址

插件完整版地址 m-lazy
https://github.com/zhanyouwei...

欢迎一起交流

欢迎关注我的个人公众号,不定期更新自己的工作心得。

正文开始

Web 图片的懒加载就是通过读取img元素,然后获得img元素的data-src(也可以约定为其他属性名)属性的值,并赋予img的src,从而实现动态加载图片的机制。

这里需要注意的是: img在初始化的时候不要设置src属性,因为即使设置 src='' 浏览器也会尝试加载图片。

一个简单的图片懒加载共涉及两个方面,

1. HTML 约定

我们首先需要给准备实施懒加载的img元素添加指定的class 这里为m-lazyload ,同时将img src赋值给 data-src属性。
具体示例为:

<img class="m-lazyload" data-src="imgUrl">

2. JavaScript 实现

动态加载总共分为以下几个步骤,这里每个步骤都将被拆分为独立的函数

1. 添加页面滚动监听事件
window.addEventListener('scroll', _delay, false);

function _delay() {
  clearTimeout(delay);
  delay = setTimeout(function () {
    _loadImage();
  }, time);
}
2. 当触发监听事件时会执行 _loadImage 函数,该函数负责加载图片
function _loadImage() {
  for (var i = imgList.length; i--;) {
    var el = imgList[i];
    if (_isShow(el)) {
      el.src = el.getAttribute('data-src');
      el.className = el.className.replace(new RegExp("(\\s|^)" + _selector.substring(1, _selector.length) + "(\\s|$)"), " ");
      imgList.splice(i, 1);
    }
  }
}

虽然执行了_loadImage函数,但是我们得知道哪些图片需要被加载,这里的判断依据是什么呢?

依据就是判断该图片是否在当前窗口的可视区域内,在这里我们封装一个_isShow函数来实现

function _isShow(el) {
  var coords = el.getBoundingClientRect();
  return ( (coords.top >= 0 && coords.left >= 0 && coords.top) <= (window.innerHeight || document.documentElement.clientHeight) + parseInt(offset));
}

自此一个图片加载的闭环就形成了

当网页滚动的事件被触发 -> 执行加载图片操作 -> 判断图片是否在可视区域内 -> 在,则动态将data-src的值赋予该图片。

太简单了?

事实就是如此!!!

如此简单,不妨扩展一下

  1. 添加一些自定义参数,谁都喜欢自定义,不是吗?

  2. 支持iScroll, iScroll是一个高性能,资源占用少,无依赖,多平台的javascript滚动插件。

这里我们做了些优化
  1. 图片加载后移除选择器,避免重复判断。

  2. 缓存img元素结合,减少dom元素查询性能损耗

  3. 扩展prototype添加getNode方法,支持分页数据懒加载(由于我们之前缓存了dom元素)

OK!说了这么多,show me the code 吧!


(function () {
  var imgList = [],  // 页面所有img元素集合
    delay,   // setTimeout 对象
    offset,  //偏移量,用于指定图片距离可视区域多少距离,进行加载
    time,  // 延迟载入时间
    _selector; // 选择器 默认为 .m-lazyload

  function _isShow(el) {
    var coords = el.getBoundingClientRect();
    return ( (coords.top >= 0 && coords.left >= 0 && coords.top) <= (window.innerHeight || document.documentElement.clientHeight) + parseInt(offset));
  }

  function _loadImage() {
    for (var i = imgList.length; i--;) {
      var el = imgList[i];
      if (_isShow(el)) {
        el.src = el.getAttribute('data-src');
        el.className = el.className.replace(new RegExp("(\\s|^)" + _selector.substring(1, _selector.length) + "(\\s|$)"), " ");
        imgList.splice(i, 1);
      }
    }
  }

  function _delay() {
    clearTimeout(delay);
    delay = setTimeout(function () {
      _loadImage();
    }, time);
  }

  function ImageLazyload(selector, options) {
    var defaults = options || {};
    offset = defaults.offset || 0;
    time = defaults.time || 250;
    _selector = selector || '.m-lazyload';
    this.getNode();
    _delay();//避免首次加载未触发touch事件,主动触发一次加载函数
    if (defaults.iScroll) {
      defaults.iScroll.on('scroll', _delay);
      defaults.iScroll.on('scrollEnd', _delay);
    } else {
      window.addEventListener('scroll', _delay, false);
    }
  }
  ImageLazyload.prototype.getNode = function () {
    imgList = [];
    var nodes = document.querySelectorAll(_selector);
    for (var i = 0, l = nodes.length; i < l; i++) {
      imgList.push(nodes[i]);
    }
  };
})();
如果觉得我的文章对你有用,请随意赞赏

26 条评论
方泉水很甜 · 2016年10月15日

我的实现是设置两个缓冲:

  • 第一个是去抖动,目的在于减少scroll触发业务的频率,同时也可以模仿 scrollEnd,实现一般可以用setTimeout;

  • 第二是:当判断 scrollEnd时,将要加载图片变成异步过程(通常也可以是setTimeout),并且放入一个队列中。当再次滚动(相隔时间较短)时,队列会被清空。最终通过这样两层调控来控制加载的时机。

+2 回复

Youwei 作者 · 2016年03月28日

Javascript 中的函数作用域是根据函数调用者来决定的

+1 回复

zhuzi · 2016年03月23日

请问为什么_loadImage()需要异步执行?

回复

Youwei 作者 · 2016年03月23日

这样做,可以避免滚动过快,造成的性能和流量的损耗

回复

阿驴 · 2016年03月25日

手机浏览器中的滚动缓冲能解决吗?

回复

Youwei 作者 · 2016年03月25日

能仔细描述一下你的问题吗?比如 滚动缓冲 会引起什么问题呢?

回复

阿驴 · 2016年03月25日

就是h5页面中我很快的滚动页面,他会很久才能停下来,这时候监听scroll事件显然是不合理了,因为只有等页面停下来那些图片才能加载出来。 这个有什么办法能处理下吗? 可以看下淘宝h5的页面 往下滚试试看效果。

回复

Youwei 作者 · 2016年03月25日

你的想法非常好!根据你的提议,我优化了一下代码,由监听scroll方法改为监听scrollEnd方法,不过原生并不支持,只能暂时在我们的项目中使用,另:我们项目中使用了iScroll插件。

目前如果想用原生实现的话,我的想法是使用setTimeout方式,监听scroll事件,比如隔300ms如果没有在滚动,则认为是scrollEnd了

回复

visibility · 2016年03月28日

ImageLazyload 方法里面的 this.getNode();这个报错不能直接这样写把?

回复

Youwei 作者 · 2016年03月28日

可以的,

回复

visibility · 2016年03月28日

好吧,是我写错了,调用的时候需要new一下那个函数,话说为什么new ImageLazyload这个函数就改变了里面的this指针如果不new this就是window对象啊。

回复

阿驴 · 2016年03月31日

把这种方式再编辑进来,让文章更完整? 嘿嘿

回复

lovehall · 2016年07月06日

楼主 你的defaults.iScroll传的是绑定iscroll方法的那个scroller对象嘛?

回复

Youwei 作者 · 2016年07月06日

是的

回复

lovehall · 2016年07月06日

你的iscroll版本是5的么 我的是4 绑定不了scrollEnd,scroll事件

回复

Youwei 作者 · 2016年07月06日

你可以自己实现一个scrollend

回复

Youwei 作者 · 2016年10月17日

Hi,我是作者,欢迎加我微信,一起交流 giszhongguo

回复

葡式蛋塔 · 2016年10月17日

return ( (coords.top >= 0 && coords.left >= 0 && coords.top) <= (window.innerHeight || document.documentElement.clientHeight) + parseInt(offset));

此处的coords.top >=0 和coords.top 有矛盾啊

回复

海农 · 2016年12月02日

iscroll高度会刷新吗?代码貌似没看到这个处理

回复

0

这个应该在业务里面自己处理的

Youwei 作者 · 5月24日
Youwei 作者 · 2016年12月02日

高度最好在页面渲染时就预先占坑,依托iscroll刷新性能非常差

回复

载入中...
Youwei Youwei

1.5k 声望

发布于专栏

前端有多难?

友伟 在 SegmentFault

23 人关注