什么是懒加载?
众所周知,网页记载中图片的加载耗时远远超过html、css、js甚至font之和,但这些图片可能并不是必须的,也许是隐藏或者还未查看的图片,让这些图片在被用户查看时再加载它们,能大大增加网页的加载速度,这就是懒加载。
开始封装
(function() {
const f = {
}
}())
将图片的路径暂存在lazy-src中,在它准备好后再将其复制到src中来加载图片,在此之前,img的src属性应指向一个空白图片或者一个加载gif动画。
<img src="svg.svg" lazy-src="img-1"
f.displsy = function(element) {
element.src = element.getAttribute('lazy-src')
}
何时加载?
当图片被显示,并且在可视范围或接近可视范围时加载图片
获取图片是否显示
隐藏元素一共只有三个方法display === 'none'、visibility === 'hidden'以及opacity === '0'
也就是说如果img和img的所有父级不满足以上三个条件则为显示状态
用window.getComputedStyle(element).style来获取指定元素的指定样式。
element.parentNode指向其父级,直到document,利用这个特性遍历元素的所有父级。
f.isDisplsy = function(element) {
let ele = element
do{
if(window.getComputedStyle(ele).display === 'none') return
if(window.getComputedStyle(ele).visibility === 'hidden') return
if(window.getComputedStyle(ele).opacity === '0') return
} while ((ele = ele.parentNode) !== document)
return true
}
检测元素是否在可见区域内或者与可见区域的距离
element.offsetLeft返回元素距离其定位父元素的距离,无论他是什么定位方式、行内或块级。
尽管element.offsetLeft已经能非常直观的表达一个精确的距离,但是它仍然会忽略两个可能会影响位置的元素:transform:translate()和scrollTop/scrollLeft。
transform:translate()由于不会改变占用空间的位置和大小,并不适合用来做布局,因此先忽略它的影响。
element.scrollLeft与element.offsetLeft所表达的方向刚好相反,我们将scrollTop/scrollLeft减去即可
用element.offsetParent来获取元素的定位父元素
稍整思路后它应该是这样的:
f.win = function(element) {
let ele = element, l = t = 0
do {
l += ele.offsetLeft - ele.scrollLeft
t += ele.offsetTop - ele.scrollTop
} while (ele = ele.offsetParent)
return { left: l, top: t }
}
到这已经可以算出元素距离页面的绝对距离了,但还需要一个重要的因素:window.scrollX/window.scrollY它将带来页面的滚动距离,减去它之后将得到元素距离页面可见区域的顶部、左侧的距离。
f.win = function(element) {
let ele = element, l = t = 0
do {
l += ele.offsetLeft - ele.scrollLeft
t += ele.offsetTop - ele.scrollTop
} while (ele = ele.offsetParent)
l -+ window.scrollX
t -= window.scrollY
return { left: l, top: t }
}
到了这一步后,我们有了一个非常直观的数据,元素顶部、左侧距离页面可见区域顶部、左侧的距离,然后将开始着手计算元素距离页面可见区域的最小距离。要计算这些,还需要两个重要的数据:元素可见区域大小和页面可见区域大小,使用element.clientWidth/element.clientHeight和window.innerWidth/window.innerHeight来获得这些数据。然后利用手上的数据来计算元素距页面可见区域上、下、左、右的距离
distance(ele) {
let win = this.win(ele)
//上
win.top
//下
win.bottom = window.innerHeight - win.top - ele.clientHeight
//左
win.left
//右
win.right = window.innerWidth - win.left - ele.clientWidth
return win
}
到这一步我们有了元素距页面可见区域上、下、左、右的距离,如果是正数表示在可见区域(哪怕只是露出一角),反之则表示其完全不可见。 但是我们并不需要这么多数据,于是我们取他们当中的最小值 这里我用了最偷懒的写法:Object.values()取对象的可枚举值然后 Math.min()取最小值
f.displsy = function(element) {
let minDistance= Math.min(...Object.values(this.distance(element)))
element.src = element.getAttribute('lazy-src')
}
但是这里有一个细节的问题,就是它返回的数据非常不符合人的直觉,比方说我希望img在距离可见区域100px时显示,我通常会传入一个100,但是事实上此时f.distance()返回值应该是-100,为了对使用者更友好,我们将传入的数取相反数再来比较
f.display(element, distance) {
let minDistance = Math.min(...Object.values(this.distance(element)))
if(-distance > minDistance) return
element.src = element.getAttribute('lazy-src')
}
顺便把isDisplay也写进判断,为了减少不必要的负担,将其写在distance判断的前面
f.display(element, distance) {
let minDistance = Math.min(...Object.values(this.distance(element)))
if(!this.isDisplay(element) || -distance > minDistance) return
element.src = element.getAttribute('lazy-src')
}
OK,到这一步已经可以完整的判断是否要加载这个img图片了,但是我们面临一个最重要的问题:事件源
什么时候判断这个图片是否要加载?定时判断?或许是个方法,简单粗暴,但是对CPU不太友好,但是能影响display判断的元素太多了,也许我们可给img的所有父元素绑定scroll事件来达到判断distance的目的,但是我们如何捕获img的css样式导致的元素显示或隐藏?
答案是:无法捕获!
原生js没有给我们提供这种方法
但是我们可以封装设置样式的方法
想想如果页面有Jquery插件,那么我们将统一使用jQuery.css()来改变样式,那么只要在jQuery.css()里面触发display的判断条件即可,他们各司其职,又相互依赖,这就是生态
那么难道没了生态的支持我们就止步于此吗?当然不是,尽管野蛮生长很艰难,但也并非走投无路,元素的css总归是js来改变,我们只需要在可能改变css的地方手动调用一遍判断方法即可。
别忘了在window的滚动事件上调用判断方法
完成封装
(function () {
const f = {
display(element, distance) {
let minDistance = Math.min(...Object.values(this.distance(element)))
if (!this.isDisplay(element) || -distance > minDistance) return
element.src = element.getAttribute('lazy-src')
console.log(true);
},
isDisplay(element) {
let ele = element
do{
if(window.getComputedStyle(ele).display === 'none') return
if(window.getComputedStyle(ele).visibility === 'hidden') return
if(window.getComputedStyle(ele).opacity === '0') return
} while ((ele = ele.parentNode) !== document)
return true
},
win(element) {
let ele = element, l = t = 0
do {
l += ele.offsetLeft - ele.scrollLeft
t += ele.offsetTop - ele.scrollTop
} while (ele = ele.offsetParent)
l - + window.scrollX
t -= window.scrollY
return { left: l, top: t }
},
distance(ele) {
let win = this.win(ele)
//上
win.top
//下
win.bottom = window.innerHeight - win.top - ele.clientHeight
//左
win.left
//右
win.right = window.innerWidth - win.left - ele.clientWidth
return win
}
}
if (window.f) {
Object.assign(window.f, f)
} else {
window.f = f
}
return window.f
}())
懒加载的完整代码将作为彩蛋随着我的原创JS库Flying3.0发布 Flying3.0将完全抛弃前作的设计思路,重写整个Flying选择器,使其看起来更像Jquery选择器,但更加丰富,全新Flying3.0将在可见的未来发布,但我目前还在测试和完善中,如果你也有兴趣欢迎联系我。