一文让你学会简单的懒加载

100 阅读4分钟

前言:前端性能优化中有一条懒加载,这是经常在面试中会被提到的。笔者在毕业出来找工作面试的时候也经常问过,所以就写下这篇文章,记录所学的知识点,一起共勉(* ̄︶ ̄)。

场景和原理

懒加载多用于图片数量多的首页展示中,因为图片过多(尤其一些高清大图),在网页第一次请求资源的时候,会直接请求全部的图片,然后多任务一起下载,网络被平分,这就导致在页面中会看到很多图片空白或者图片都是一点点加载出来的,这对于用户体验是很不好的。

懒加载也就是延迟加载,当访问一个页面的时候,先把img元素或是其他元素的背景图片路径替换成一张大小为1*1px图片的路径(这样就只需请求一次,俗称占位图),或者使用骨架屏来暂时展示。 只有当图片出现在浏览器的可视区域内时,才设置图片正真的路径,让图片显示出来。这就是图片懒加载。

步骤:

  1. 在img标签中加多自定义属性,例如data-src来存放真正的图片路径,而src里面统一放默认提示图片
  2. 使用document.querySelectorAll来获取所有img标签的实例
  3. 获取当前屏幕的视口高度,判断滚动条滚过的距离+视口高度是否大于图片距离顶部的距离,如果大于则动态将data-src属性的值赋值给src
  4. 绑定 window 的 scroll 事件,对其进行事件监听

在正式代码实例开始前,笔者先简单讲解下前面高度的问题

各个高度获取与区别

这里笔者只讲述所用的几个高度

  • 窗口高度:innerHeight表示窗口内容区域的高度,这是不包括边框、菜单栏的。
  • 获取滚动条位置:document.documentElement.scrollTop
  • 返回元素的高度:clientHeight
  • offsetTop:获取对象相对于版面或由 offsetTop 属性指定的父坐标的计算顶端位置
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
        //由于不同浏览器对html标签的默认外边距和内边距的值不同,为了在主流浏览器中统一标准让布局显示相同,需要将html所有标签的padding,margin都设置为0
            margin: 0;
            padding: 0;
        }
        body{
            /* 这里需要使用绝对定位来固定住body,不然会受子元素的margin元素影响,可以把下面注释掉看看效果 */
            position: absolute;
        }
        .parent {
            width: 500px;
            height: 1500px;
            margin: 100px auto;
            background-color: red;
            border: 10px solid #000;
            overflow: hidden;
        }
        .child {
            width: 300px;
            height: 300px;
            border: 1px solid #000;
            padding: 10px;
            margin: 50px 90px;
            background-color: green;
        }
        .content{
            height: 500px;
            width: 400px;
            padding: 30px;
        }
    </style>
</head>

<body>
    <div style="height: 200vh;width: 90vw;">
        <div class="parent">
            <div class="content">
                <div class="child"></div>
            </div>
        </div>
    </div>
    <script>
        var child = document.querySelector('.child');
        var body = document.querySelector('body')
        var html = '';
        html += "offsetHeight=" + child.offsetHeight + "<br>";
        html += "offsetTop=" + child.offsetTop + "<br>";
        html += "winHeight=" + window.innerHeight + "<br>";
        html += "clientHeight=" + child.clientHeight;
        child.innerHTML = html;
        // 获取各个高度
        function getHeight() {
            var scrollTop = document.documentElement.scrollTop;
            console.log('滚动条经过的距离', scrollTop)
            console.log('body的高', body.clientHeight) //使用这个来获取高度
            console.log('元素距离顶部的距离',child.offsetTop)
            w = window.innerWidth
            h = window.innerHeight
            console.log('视口的宽高:', w, h)   // 1440 757
        }
        window.addEventListener('scroll', getHeight)
    </script>
</body>
</html>

lazy.png

我们拿到了页面区域的总高度,那么可能有人好奇offsetTop这个值是怎么计算来的

无标题.png

从上面得出offsetTop的值,但是这里会有人发现,border的值是没计算到的,如果是要精确,建议在得出offsetTop的值后再依次加上每个盒子的border值。笔者这里是偷懒加上border值加起来偏差不大就没计算。

利用JS获取元素的宽高来实现

<!DOCTYPE html>
<html>

<head>
	<meta charset="utf-8">
	<title></title>
</head>

<body>
	<div class="container">
		<div style="height: 150vh;"></div>
		<img src="default.jpg" data-src="./img/3.png">
		<div style="height: 10vh;"></div>
		<img src="default.jpg" data-src="./img/4.jpg">
	</div>
	<script>
		var imgs = document.querySelectorAll('img');
		function lozyLoad() {
			//通关监听scroll事件来判断图片是否到达视口
			//视口高度+滚动条滑过的高度》图片到达顶部的高度,则图片展示
			var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
			var winHeight = window.innerHeight;
			console.log(winHeight)
			for (var i = 0; i < imgs.length; i++) {
				if (imgs[i].offsetTop < scrollTop + winHeight) {
					console.log('图片距离顶部的距离:',imgs[i].offsetTop)
					console.log('滚动条滑过的距离+视口高度:',scrollTop+winHeight)
					imgs[i].src = imgs[i].getAttribute('data-src');
				}
			}
		}
		function throttle(fn, delay) {
			let last = 0;
			return function (...args) {
				let now = new Date()
				if (now - last > delay) {
					fn.apply(this)
					last = now
				}
			}
		}
		//利用节流减少scroll事件的频繁触发
		window.addEventListener('scroll', throttle(lozyLoad, 200))
                </script>
        <style>
		.container {
			height: 400vh;
		}
	</style>
</body>

</html>

lazy2.png

利用getBoundingClientRect来实现

getBoundingClientRect()是DOM元素到浏览器可视范围的距离(不包含文档卷起的部分)。

该函数返回一个Object对象,该对象有6个属性:top,lef,right,bottom,width,height; 

top:元素上边到视窗上边的距离

bottom:元素下边到视窗上边的距离

right:元素右边到视窗左边的距离

left:元素左边到视窗左边的距离

width:元素自身的宽

height:元素自身的高

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title></title>
</head>
<body>
	<div class="container">
		<div style="height: 150vh;"></div>
		<img src="default.jpg" data-src="./img/3.png">
		<div style="height: 10vh;"></div>
		<img src="default.jpg" data-src="./img/4.jpg">
	</div>
	<script>
        function throttle(fn, delay) {
			let last = 0;
			return function (...args) {
				let now = new Date()
				if (now - last > delay) {
					fn.apply(this)
					last = now
				}
			}
		}
                // DOM 元素的 `getBoundingClientRect` API。
		let img = document.getElementsByTagName("img");
		let num = img.length;
		let count = 0;//计数器,从第一张图片开始计
		console.log(img[0].getBoundingClientRect())
		console.log(img[1].getBoundingClientRect())
		function lazyload2() {
			for (let i = count; i < num; i++) {
				//元素现在已经出现在视口中
				//返回元素的大小以及相对于视口的位置
				// console.log(img[i].getBoundingClientRect().top)
				//可见区域高度
				// console.log(document.documentElement.clientHeight)
				if (img[i].getBoundingClientRect().top < document.documentElement.clientHeight) {
					if (img[i].getAttribute("src") !== "default.jpg") continue;
					img[i].src = img[i].getAttribute("data-src");
					count++
				}
			}
		}
		window.addEventListener('scroll',throttle(lazyload2,200))
                </script>
	<style>
		.container {
			height: 400vh;
		}
	</style>
</body>

</html>

使用IntersectionObserver来实现

IntersectionObserver 对象,用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见。 MDN介绍是:IntersectionObserver接口提供了一种异步观察目标元素与祖先元素或顶级文档viewport的交集中的变化的方法。祖先元素与视窗viewport被称为根(root)。

具体了解该API可通过这里前往

IntersectionObserver支持两个参数:

var observer = new IntersectionObserver(callback,options);

callback是当被监听元素的可见性变化时,触发的回调函数

options是一个配置参数,可选,有默认的属性值

这里用到callback实例中的isIntersecting属性

isIntersecting:返回一个布尔值,下列两种操作均会触发callback:1. 如果目标元素出现在root可视区,返回true。2. 如果从root可视区消失,返回false

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title></title>
</head>
<body>
	<div class="container">
		<div style="height: 150vh;"></div>
		<img src="default.jpg" data-src="./img/3.png">
		<div style="height: 10vh;"></div>
		<img src="default.jpg" data-src="./img/4.jpg">
	</div>
	<script>
        		let img = document.getElementsByTagName("img")
		//IntersectionObserver.observe() 开始监听一个目标元素。
		//提供了一种异步观察目标元素与其祖先元素或顶级文档视窗交叉状态的方法
		
		const observer = new IntersectionObserver(changes=>{
			console.log(changes)
			//changes是被观察的元素集合
			for(let i =0,len=changes.length;i<len;i++){
				let change = changes[i]
				//通过这个属性判断是否在视口中
				if(change.isIntersecting){
					const imgElement = change.target;
					imgElement.src = imgElement.getAttribute("data-src")
					//停止监听特定目标元素。
					observer.unobserve(imgElement)
				}
			}
		})
          
          //获取的是一个类数组对象,需要转换为数组然后循环加上监听
	Array.from(img).forEach(item=>observer.observe(item))
        </script>
	<style>
		.container {
			height: 400vh;
		}
	</style>
</body>
</html>

效果截图我就不贴出了,读者们可以运行代码,在控制台慢慢去实验,自己动手去思考才能收获更多东西

总结:一开始研究懒加载发现实现简单的懒加载还挺容易的,但是后面去了解浏览器各个宽高的时候属实搞得有点晕。这里推荐下这篇文章关于浏览器宽高获取以及讲解。在不断去寻找资料的同时也不断在积累,这就是所谓成长。如果这篇文章对你有收获,我将会十分开心。如果有不足之处,请各位大佬不吝赐教(* ̄︶ ̄)。