jquery lazyload源码分析
这是一款很老很有名的插件了,专门用于图片延迟加载,github地址如下:
https://github.com/tuupola/jquery_lazyload
下面,我摘取比较重要的代码说明一下它的实现思路,代码量非常小10分钟就可以看完:
基础原理
如果一个网页很高、图片很多,那么浏览器就会并行的下载图片,对网速一般的用户是体验很差的,为什么呢?
- 图片加载是并发的异步的,不同位置的图片加载完成的次序就会不同,可能导致网页上面的图片还没下载完,后面的就完成了,造成一种不流畅感。
- 正因为图片并发下载,用户带宽和服务器带宽有限的情况下,这样的资源竞争可能导致整体加载时间拖长。
那么图片延迟加载是怎么优化这个问题呢?
很简单,当图片区域进入视野的时候才让其进行下载,那怎么实现呢?我们知道,<img src=””>一旦设置src属性,浏览器就会启动下载,因此插件的思路是不设置src属性,而是等待<img>区域进入视野后再设置src属性。
因此,lazyload插件让我们把图片的url写在其他属性里:
1 |
<img class="lazy" data-original="img/example.jpg" width="640" height="480"> |
加载时机
插件会在浏览器ready事件触发后,搜集所有<img>标签保存成数组,现在的问题就变成了:什么时机设置这些<img>的src属性呢?
插件选择了2个时机,一个是浏览器的scroll事件,一个是浏览器的resize事件,意图很容易理解,通常只有这2个行为会导致图片从视野之外进入到视野之内。
滚动事件scroll
1 2 3 4 5 6 |
/* Fire one scroll event per scroll. Not one scroll event per image. */ if (0 === settings.event.indexOf("scroll")) { $container.on(settings.event, function() { return update(); }); } |
窗口尺寸变化resize
1 2 3 4 |
/* Check if something appears when window is resized. */ $window.on("resize", function() { update(); }); |
检测方法
当上面的事件发生的时候,我们知道可能某些图片进入了视野,那么具体是哪些图片进入了视野呢?
那就是update方法的事情了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function update() { var counter = 0; elements.each(function() { var $this = $(this); if (settings.skip_invisible && !$this.is(":visible")) { return; } if ($.abovethetop(this, settings) || $.leftofbegin(this, settings)) { /* Nothing. */ } else if (!$.belowthefold(this, settings) && !$.rightoffold(this, settings)) { $this.trigger("appear"); /* if we found an image we'll load, reset the counter */ counter = 0; } else { if (++counter > settings.failure_limit) { return false; } } }); } |
elements是网页中所有的<img>标签数组(Jquery数组),遍历所有<img>标签判断哪些<img>在视野范围内即可。
- 第一个if分支:表明该<img>在屏幕上方之外或者在屏幕左侧之外则什么也不用做,否则说明<img>在屏幕上方之内与屏幕左侧之内,需进入下面的第二个分支。
- 第二个if分支:判断<img>是否在屏幕下方之外或者屏幕右侧之外,如果不是则说明<img>就在视野范围内了,立即给<img>触发自定义的appear事件以便进一步处理。
- 第三个if分支:如果<img>在屏幕下方或者屏幕右侧之外,则停止each遍历,这是什么意图呢?为了减少遍历的性能损耗,下面具体说说。
首先,这个选项的出现主要是因为每次滚动scroll都要遍历elements数组中所有的img,判断它们哪些进入了视野,这本身是很耗费性能的,因此如何机智的停止遍历就很重要了。
elements数组最初是采用jquery的选择器语法获取的,它按照深度遍历方式从上至下,从外至内的遍历DOM节点搜集img标签,因此当发现图片在屏幕下方或右侧之外的时候,作者认为剩余的图片按照常规的DOM顺序都应该在视野之外,就不必再继续判断后续的img了(可以参考github issue)。
当然,作者这个优化的前提是DOM结构比较简单(比如自上而下的新闻图片),对于复杂的DOM结构(比如纵横2个方向都有滚动条)是可能出现误判导致视野内的图片无法加载的,因此该插件可以提供一个阀值failure_limit来控制是否可以额外探测一些img标签。
加载图片
其实在初始化插件的时候,已经为elements数组里的所有<img>绑定了appear事件处理函数:
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 |
this.each(function() { var self = this; var $self = $(self); self.loaded = false; /* If no src attribute given use data:uri. */ if ($self.attr("src") === undefined || $self.attr("src") === false) { if ($self.is("img")) { $self.attr("src", settings.placeholder); } } /* When appear is triggered load original image. */ $self.one("appear", function() { if (!this.loaded) { if (settings.appear) { var elements_left = elements.length; settings.appear.call(self, elements_left, settings); } $("<img />") .one("load", function() { var original = $self.attr("data-" + settings.data_attribute); $self.hide(); if ($self.is("img")) { $self.attr("src", original); } else { $self.css("background-image", "url('" + original + "')"); } $self[settings.effect](settings.effect_speed); self.loaded = true; /* Remove image from array so it is not looped next time. */ var temp = $.grep(elements, function(element) { return !element.loaded; }); elements = $(temp); if (settings.load) { var elements_left = elements.length; settings.load.call(self, elements_left, settings); } }) .attr("src", $self.attr("data-" + settings.data_attribute)); } }); |
这里this是用户传入的的jquery元素选择器,等价于elements数组,收集了符合选择器的<img>标签。
当一个<img>标签触发了appear事件后,说明该<img>已经进入视野,因此插件会通过$(‘<img />’)创建一个临时的img DOM节点,并设置它的src属性为真实图片地址,这样浏览器就会去下载这个图片了。
一定要注意,这里并不是直接设置<img>标签的src,而是创建一个临时img并设置其src从而下载图片,这样做的目的是为了有机会控制<img>图片的淡入淡出效果。
当临时img下载图片完成时,浏览器会触发load事件通知图片加载完成,这时候浏览器已经将图片保存在本地并且缓存,因此这时候给原始的<img>设置src属性可以保障图片立即渲染。
插件提供图片的淡入效果,具体做法就是先把<img>通过hide()隐藏起来,然后接着调用了可配置的过渡函数,插件默认调用show()方法,并传入一个过渡动画时间参数,这样就实现了淡出的感觉。
当然,接下来要标记这个<img>的loaded=true,这背后也是有意图的:当一个未加载的<img>进入视野后,插件创建临时img进行图片加载,但是我们知道下载图片是需要时间的,极有可能load完成回调可迟迟没有到来,这时候再次滚动会再次触发该<img>的appear事件,按逻辑又会创建另外一个临时img加载同样的图片,因此第一个加载完成的回调中将loaded设置为true,可以跳过后续重复的没有意义的load回调逻辑。
另外,为了减少elements的大小,从而减小每次update函数的遍历成本,所有加载成功的<img>标签都将从elements数组中移除,这里用的jquery的grep方法将返回一个过滤后的数组,而$(数组)将返回一个jquery包装的elements数组对象,可以参考grep方法说明。
关于jquery lazyload插件就说到这里,另外值得参考的是lazyload封装成jquery插件的方式。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
