图片懒加载

21,789 阅读3分钟

前言 

图片懒加载在一些图片密集型的网站中运用比较多,通过图片懒加载可以让一些不可视的图片不去加载,避免一次性加载过多的图片导致请求阻塞(浏览器一般对同一域名下的并发请求的连接数有限制),这样就可以提高网站的加载速度,提高用户体验。 

效果预览 


如何做 

第一步: 首先我们需要让我们html中需要懒加载的img标签的src设置缩略图或者不设置src,然后自定义一个属性,值为真正的图片或者原图的地址(比如下面的data-src),并且定义一个类名,表示该图片是需要懒加载的(比如下面例子的lazy-image),这有两个作用: 

1、为以后获取需要懒加载图片的img元素 

2、可以给这个类名设置背景图片,作为图片未加载前的过度图片,比如显示为loading的图片。 

<img data-src="https://tb1.bdstatic.com/tb/cms/liveshow/ent_slid2.jpg" class="lazy-image"/> 
// css部分 
.lazy-image { 
    background: url('../img/loading.gif') no-repeat center; 
} 

第二步:页面加载完后,我们需要获取所有需要懒加载的图片的元素集合,判断是否在可视区域,如果是在可视区域的话,设置元素的src属性值为真正图片的地址。

inViewShow() {     
    let imageElements = Array.prototype.slice.call(document.querySelectorAll('.lazy-image'))    
    let len = imageElements.length     
    for(let i = 0; i < len; i++) {         
        let imageElement = imageElements[i]        
        const rect = imageElement.getBoundingClientRect() // 出现在视野的时候加载图片         
        if(rect.top < document.documentElement.clientHeight) {             
            imageElement.src = imageElement.dataset.src // 移除掉已经显示的             
            imageElements.splice(i, 1)             
            len--             
            i--         
        }     
    } 
}

这里判断是否出现在可视区域内,是通过获取元素的getBoundingClientRect属性的top值和页面的clientHeight进行对比,如果top值小于clientHeight,则说明元素出现在可视区域了。BoundingClientRect是获取某个元素相对于视窗的位置集合,见下图,注意bottomright和我们平时的rightbottom不一样。 


第三步:当用户滚动窗口的时候,遍历所有需要懒加载的元素,通过每个元素的BoundingClientRect属性来判断元素是否出现在可视区域内,判断方法同第二步一样。 

document.addEventListener('scroll', inViewShow)

这里我们可以优化下,可以通过函数节流优化滚动事件的处理函数。

利用高级特性Intersection Observer来判断元素是否可见

上面我们利用元素的BoundingClientRecttop属性和body的clientHeight来判断元素是否可见,这种传统方式获取元素是否可见的一个缺点是我们还需要绑定scroll事件,scroll事件是伴随着大量计算的,会造成资源浪费,虽然我们可以通过节流函数来提高性能,但还是会有性能浪费的问题,而Intersection Observer可以不用监听scroll事件,做到元素一可见便调用回调,在回调里面我们来判断元素是否可见。

if ("IntersectionObserver" in window) {        
    let lazyImageObserver = new IntersectionObserver((entries, observer) => {          
        entries.forEach((entry, index) => {            
            // 如果元素可见            
            if (entry.intersectionRatio > 0) {              
                let lazyImage = entry.target              
                lazyImage.src = lazyImage.dataset.src              
                lazyImage.classList.remove("lazy-image")              
                lazyImageObserver.unobserve(lazyImage)              
                // this.lazyImages.splice(index, 1)            
            }          
        })        
    })        
    this.lazyImages.forEach(function(lazyImage) {          
        lazyImageObserver.observe(lazyImage);        
    })      
}

完整代码

class LazyImage {    
    constructor(selector) {      
    // 懒记载图片列表,将伪数组转为数组,以便可以使用数组的api      
        this.imageElements = Array.prototype.slice.call(document.querySelectorAll(selector))
        this.init()    
    }      
    inViewShow() {      
        let len = this.imageElements.length      
        for(let i = 0; i < len; i++) {        
            let imageElement = this.imageElements[i]        
            const rect = imageElement.getBoundingClientRect()        
            // 出现在视野的时候加载图片        
            if(rect.top < document.documentElement.clientHeight) {          
                imageElement.src = imageElement.dataset.src          
                // 移除掉已经显示的          
                this.imageElements.splice(i, 1)          
                len--          
                i--          
                if(this.imageElements.length === 0) {            
                   // 如果全部都加载完 则去掉滚动事件监听            
                    document.removeEventListener('scroll', this._throttleFn)         
                 }        
            }      
        }    
    }      
    throttle(fn, delay = 15, mustRun = 30) {      
        let t_start = null     
        let timer = null      
        let context = this      
        return function() {        
            let t_current = +(new Date())        
            let args = Array.prototype.slice.call(arguments)        
            clearTimeout(timer)        
            if(!t_start) {          
                t_start = t_current        
            }        
            if(t_current - t_start > mustRun) {          
                fn.apply(context, args)          
                t_start = t_current        
            } else {          
                timer = setTimeout(() => {            
                    fn.apply(context, args)          
                }, delay)        
            }      
        }    
    }      
    init() {      
       // 通过IntersectionObserver api判断图片是否出现在可视区域内,不需要监听Scroll来判断      
       if ("IntersectionObserver" in window) {        
            let lazyImageObserver = new IntersectionObserver((entries, observer) => { 
                 entries.forEach((entry, index) => {            
                    // 如果元素可见            
                    if (entry.isIntersecting) {              
                        let lazyImage = entry.target              
                        lazyImage.src = lazyImage.dataset.src              
                        lazyImage.classList.remove("lazy-image")              
                        lazyImageObserver.unobserve(lazyImage)              
                        // this.lazyImages.splice(index, 1)            
                    }          
                })        
            })        
            this.lazyImages.forEach(function(lazyImage) {          
                lazyImageObserver.observe(lazyImage);        
            })      
    } else {        
        this.inViewShow()        
        this._throttleFn = this.throttle(this.inViewShow)        
        document.addEventListener('scroll', this._throttleFn.bind(this))      
    }
  }  
}
// 调用例子
new LazyImage('.lazy-image')

 git地址:github.com/VikiLee/Laz…