记得以前手机端有个业务时将用户上传的的图片在用户往上滑动手机的时候呈现,由于刚开始用户数量少就没留意,直接将后台的数据拉过来渲染,当时后台也没有分页,可是后来参与用户多了起来,跳转到这个页面就渲染个几十几百张的去请求数据,然后服务器就崩了,所以这种情况就需要用到懒加载。当需要的时候再发送图片请求,避免打开页面加载过多资源。
原理介绍:
我们使用img标签来显示图片,img标签有src属性用来表示图片资源,当src属性为空或者不存在时,浏览器就不会去请求资源,当我们给img的src赋值时浏览器就去拿到资源并显示。
所以可以利用这个特点,先不给img元素设置src属性,将URL先存放在data-src自定义属性中,只有当img元素即将进入可视区的时候再将data-src赋值给src。浏览器请求资源显示实现懒加载。
HTML结构:alt属性用于显示图片无法显示时替代文本,data-src属性存储真实的url地址
<div class="container">
<div class="img-area">
<img class="my-photo" alt="loading" data-src="./img/img1.png">
</div>
<div class="img-area">
<img class="my-photo" alt="loading" data-src="./img/img2.png">
</div>
<div class="img-area">
<img class="my-photo" alt="loading" data-src="./img/img3.png">
</div>
</div>
通过元素px等单位判断img和可视区的距离来确定加载时机
一.利用offsetTop、scrollTop、以及可视区距离文档顶部的距离
- 获取屏幕可视窗口高度:document.documentElement.clientHeight
- 获取观测元素相对于文档顶部的距离:element.offsetTop
- 获取可视窗口距离文档顶部的距离即滚动条的距离(scrollTop):document.documentElement.scrollTop
如上图所示,ruduce距离就是上滑不断减小的距离即将进入可视区的距离,当reduce<=0时进入可视区。即:
reduce = offsetTop - scrollTop - clientHeight <= 0就在可视区了。
二.Element.getBoundingClientRect()方法
Element.getBoundingClientRect()方法返回元素的大小及其相对于视口的位置。返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects()
方法返回的一组矩形的集合, 即:是与该元素相关的CSS 边框集合 。DOMRect 对象包含了一组用于描述边框的只读属性——left、top、right和bottom,单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的。
从上面的图中可以看出,top值总是相对于视口的左上角的,当Top值和视口值相等时(Top===clientHeight)即元素上沿到达进入视口的临界点,继续滚动进入可视区即:Top<=clientHeight
判断条件:
function isInSight(el){
const bound = el.getBoundingClientRect();
const clientHeight = window.innerHeight;
return bound.top <= clientHeight + 100;
}
这里+100是提前加载,可以自行定义提前多少距离就进行加载
加载图片:
页面打开时,对所有在可视区内的图片进行检查,如果在可视区内就尽心加载
//加载可视区的图片
function checkImgs(){
const imgs = document.querySelectorAll('.my-photo');
Array.from(imgs).forEach(img=>{
if(isInSight(img)){
loadImg(img);
}
})
}
//可以进行优化,已经加载的图片在滚动条滚动的时候就不再去选择遍历了,设置一个index,只遍历未加载的图片
let index = 0;
function checkImgs(){
const imgs = document.querySelectorAll('.my-photo');
for(let i=index;i<imgs.length;i++){
loadImg(imgs[i]);
index = i;
}
}
//加载图片
function loadImg(img){
if(!el.src){
const source = img.dataset.src;
img.src = source;
}
}
函数节流:
既然会存在滚动按需加载频繁操作DOM的情况就需要用到函数节流。即让一目标函数不要执行的太频繁,减少过快的调用来节流。
基本步骤:
- 获取第一次触发事件的时间戳
- 获取第二次触发事件的时间戳
- 时间差如果大于某个阈值就执行目标函数,然后重置第一个时间戳
function throttle(fn,mustRun=500){
let previous = null;
return function(){
let now = new Date();
let context = this;
let args = arguments;
if(!previous){
previous = now;
fn.apply(context,args);
}
let remaining = now - previous;
if(mustRun && remaining>=mustRun){
fn.apply(context,args);
previous = now;
}
}
}
完整代码:
<body>
<div class="container">
<div class="img-area">
<img class="my-photo" alt="loading" data-src="./img/img1.png">
</div>
<div class="img-area">
<img class="my-photo" alt="loading" data-src="./img/img2.png">
</div>
<div class="img-area">
<img class="my-photo" alt="loading" data-src="./img/img3.png">
</div>
</div>
<script>
//判断是否在可视区
function isInSight(el) {
const bound = el.getBoundingClientRect();
const clientHeight = window.innerHeight;
return bound.top <= clientHeight + 100;
}
//判断图片是否在可视区,是就加载
let index = 0;
function checkImgs() {
const imgs = document.querySelectorAll('.my-photo');
for (let i = index; i < imgs.length; i++) {
if (isInSight(imgs[i])) {
loadImg(imgs[i]);
index = i;
}
}
}
//加载图片
function loadImg(el) {
if (!el.src) {
const source = el.dataset.src;
el.src = source;
}
}
//函数节流
function throttle(fn, mustRun = 500) {
let previous = null;
return function() {
const now = new Date();
const context = this;
const args = arguments;
if (!previous) {
previous = now;
fn.apply(context,args);
}
const remaining = now - previous;
if (mustRun && remaining >= mustRun) {
fn.apply(context, args);
previous = now;
}
}
}
//初次调用
window.onload=checkImgs;
//滚动时调用
window.onscroll = throttle(checkImgs);
</script>
</body>
三.使用IntersectionOberserver观察器
目前有一个新的 IntersectionObserver API,可以自动"观察"元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做"交叉观察器"。
API
它的用法非常简单。
var io = new IntersectionObserver(callback, option);
上面代码中,IntersectionObserver
是浏览器原生提供的构造函数,接受两个参数:callback
是可见性变化时的回调函数,option
是配置对象(该参数可选)。
构造函数的返回值是一个观察器实例。实例的observe
方法可以指定观察哪个 DOM 节点。
// 开始观察 io.observe(document.getElementById('example')); // 停止观察 io.unobserve(element); // 关闭观察器 io.disconnect();
上面代码中,observe
的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。
io.observe(elementA); io.observe(elementB);
callback 参数
目标元素的可见性变化时,就会调用观察器的回调函数callback
。
callback
一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。
var io = new IntersectionObserver( entries => { console.log(entries); } );
上面代码中,回调函数采用的是箭头函数的写法。callback
函数的参数(entries
)是一个数组,每个成员都是一个IntersectionObserverEntry
对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries
数组就会有两个成员。
IntersectionObserverEntry 对象
IntersectionObserverEntry
对象提供目标元素的信息,一共有六个属性。
time
:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒target
:被观察的目标元素,是一个 DOM 节点对象rootBounds
:根元素的矩形区域的信息,getBoundingClientRect()
方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
boundingClientRect
:目标元素的矩形区域的信息intersectionRect
:目标元素与视口(或根元素)的交叉区域的信息intersectionRatio
:目标元素的可见比例,即intersectionRect
占boundingClientRect
的比例,完全可见时为1
,完全不可见时小于等于0
实现:
//实例化
const observer = new IntersectionObserver(function(changes) {
changes.forEach(function(change, index) {
// 当这个值大于0,说明满足我们的加载条件了,这个值可通过rootMargin手动设置
if (change.intersectionRatio > 0) {
// 放弃监听,防止性能浪费,并加载图片。
observer.unobserve(change.target);
//加载图片
//change.target.src = change.target.dataset.src;
loadImg(change.target);
}
});
});
//对获取元素进行监听
function initObserver() {
const listItems = document.querySelectorAll('.list-item-img');
listItems.forEach(function(item) {
// 对每个list元素进行监听
observer.observe(item);
});
}
initObserver();