原生JS实现img图片懒加载 从零开始封装懒加载算法 懒加载算法详解

320 阅读2分钟

什么是懒加载?

众所周知,网页记载中图片的加载耗时远远超过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/scrollLefttransform:translate()由于不会改变占用空间的位置和大小,并不适合用来做布局,因此先忽略它的影响。 element.scrollLeftelement.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.clientHeightwindow.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将在可见的未来发布,但我目前还在测试和完善中,如果你也有兴趣欢迎联系我。