本文所有代码-github(src/demo/example)
图片延迟加载的意义:
项目中,如果一开始加载页面,就把所有的真实图片也去加载,不论是从网络消耗上,还是从页面渲染上都是非常的消耗性能的,导致加载过慢。
真实开发中,我们一般首次渲染,不去渲染真实的图片,把图片部分用一个默认的盒子占位(或者放一个默认的正在加载中背景图)然后当img标签完全出现在视口当中再进行图片加载。
实现demo
方法一: getBoundingClientRect
传统方案
原理:借用api getBoundingClientRect ,盒子底边距离视口上面的距离 bottom 小于等于视口的高度(屏幕的高度),就算完全出来了。
<style>
html,
body {
height: 300%;
}
。lazyImageBox {
position: absolute;
left: 50%;
top: 1500px;
transform: translateX(-50%);
width: 400px;
height: 300px;
background: url("。/images/default。gif") no-repeat center center #EEE;
}
。lazyImageBox img {
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 。3s;
}
</style>
<div class="lazyImageBox">
<img src="" alt="" lazy-image="images/12.jpg">
</div>
function throttle(func, wait = 500) {
let timer = null,
previous = 0;
return function anonymous(...params) {
let now = new Date(),
remaining = wait - (now - previous);
if (remaining <= 0) {
clearTimeout(timer);
timer = null;
previous = now;
func.call(this, ...params);
} else if (!timer) {
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
previous = new Date();
func.call(this, ...params);
}, remaining);
}
};
}
let lazyImageBox = document.querySelector('.lazyImageBox'),
lazyImage = lazyImageBox.querySelector('img');
const singleLazy = function singleLazy() {
let trueImg = lazyImage.getAttribute('lazy-image');
lazyImage.src = trueImg;
lazyImage.onload = () => {
// 真实图片加载成功
lazyImage.style.opacity = 1;
};
lazyImageBox.isLoad = true;
};
const lazyFunc = function lazyFunc() {
console.log('OK');
// 防止重复处理
if (lazyImageBox.isLoad) return;
let A = lazyImageBox.getBoundingClientRect().bottom,
B = document.documentElement.clientHeight;
if (A <= B) {
singleLazy();
}
};
setTimeout(lazyFunc, 1000);
// window.onscroll = lazyFunc;
//默认浏览器会在最快的反应时间内,监听到scroll事件的触发,从而执行lazyFunc这个方法,这样导致触发频率太高了
//节流处理
window.onscroll = throttle(lazyFunc);
方法二: IntersectionObserver DOM监听器
原理:监听器 IntersectionObserver 会监听一个或者多个DOM元素和可视窗口的交叉信息。传入一个函数,当信息变化,执行回调函数,传入参数 changes , changes 一个数组,包含所有监听的DOM元素和视口的交叉信息。例如第一个监听的元素, changes[0].isIntersecting 表示是否出现在视口中。默认第一次加载完成会触发一次,当出现在视口中又会触发一次,当消失在视口中,又会触发一次。
第二个参数是配置,即出现在视口多少的时候触发回调函数。例如 {threshold: [0,0.5]} 会在刚出现时触发,出现到一半时触发。
不需要节流,这个监听器内部自动进行了优化操作。
根据这个原理,代码如下
let lazyImageBox = document.querySelector('.lazyImageBox'),
lazyImage = lazyImageBox.querySelector('img');
const singleLazy = function singleLazy() {
let trueImg = lazyImage.getAttribute('lazy-image');
lazyImage.src = trueImg;
lazyImage.onload = () => {
lazyImage.style.opacity = 1;
};
};
// 使用DOM监听器 IntersectionObserver:监听一个或者多个DOM元素和可视窗口的交叉信息
let ob = new IntersectionObserver(changes => {
// changes是一个数组,包含所有监听的DOM元素和视口的交叉信息
let item = changes[0],
{
isIntersecting,
target//目标DOM
} = item;
if (isIntersecting) {
// 完全出现在视口中了
singleLazy();
ob.unobserve(lazyImageBox); //加载真实图片后,移除对盒子的监听
}
}, {
threshold: [1]
});
ob.observe(lazyImageBox);
// ob.observe(lazyImageBox); //默认监听的时候是去重的,不需要重复坚挺
这个api在ie不兼容,在移动端是主要的应用方式。我们封装插件的时候,采用这种方式实现
封装成插件
封装的一些原则:
- 易用性
- 调用简单
- 不需要太多的依赖(最好是零依赖)
- 各种容错处理和完善的错误提示
- 详细的说明文档和各种情况的参考DEMO
- 强大
- 功能强大,项目中常现的效果,基本都可以支持
- 适配更多的需求
- 更多的用户自定义扩展(样式/功能)
- 升级及向后兼容(学习成本低)
- 高性能(性能优化、轻量级(代码少、体积小))
- 可维护性(各种设计模式的应用)
首先使用工厂模式,让导出的函数即可以当成一个类执行,也可以当做普通函数执行。当作普通函数执行的时候,也可以创造了它本身这个类的一个实例
const lz = LazyImage()
const lz = new LazyImage()
二者作用一样
完整封装代码,具体的逻辑都在注释中:
(function () {
function LazyImage(options) {
//这样即能使用函数直接生成实例,又能当作构造函数使用
return new LazyImage.prototype.init(options);
}
LazyImage.prototype = {
constructor: LazyImage,
init: function init(options) {//使用jQuery的工厂模式,这个init里的逻辑,其实就相当于原来的构造函数中的逻辑,因为最后要new init
// init params 合并config
options = options || {};
let defaults = {
context: document,
attr: 'lazy-image',
threshold: 1,
speed: 300,
callback: Function.prototype
};
let config = Object.assign(defaults, options)
// 把信息挂在到实例上:在其它方法中,基于实例即可获取这些信息
this.config = config;
this.imageBoxList = [];
// 创建监听器
const oboptions = {
threshold: [config.threshold]
};
this.ob = new IntersectionObserver(changes => {
changes.forEach(item => {
let {
isIntersecting,
target
} = item;
if (isIntersecting) {
this.singleHandle(target);
this.ob.unobserve(target);//已经加载过了,就取消监听
}
});
}, oboptions);
this.observeAll();//监听所有
},
// 单张图片的延迟加载
singleHandle: function singleHandle(imgBox) {
let config = this.config,
imgObj = imgBox.querySelector('img'),
trueImage = imgObj.getAttribute(config.attr);
imgObj.src = trueImage;
imgObj.removeAttribute(config.attr);
imgObj.onload = () => {
imgObj.style.transition = ` opacity ${config.speed}ms ` ;
imgObj.style.opacity = 1;
// 回调函数->插件的生命周期函数「回调函数 & 发布订阅」
config.callback.call(this, imgObj);
};
},
// 监听需要的DOM元素
observeAll(refresh) {
let config = this.config,
allImages = config.context.querySelectorAll( ` img[${config.attr}] ` );
[].forEach.call(allImages, item => {
let imageBox = item.parentNode;
//list里面已经监听了这个盒子,就不监听了
if (refresh && this.imageBoxList.includes(imageBox)) return;
//还没监听的,放到list里面,然后监听,以后用来和refresh对比
this.imageBoxList.push(imageBox);
this.ob.observe(imageBox);//监听盒子
});
},
// 刷新:获取新增的需要延迟加载的图片,做延迟加载
refresh: function refresh() {
this.observeAll(true);
}
};
//因为我们最后new的是init,所以需要把LazyImage.prototype指定到init上
LazyImage.prototype.init.prototype = LazyImage.prototype;
if (typeof window !== "undefined") {
window.LazyImage = LazyImage;
}
if (typeof module === "object" && typeof module.exports === "object") {
//支持commonjs和es6module规范
module.exports = LazyImage;
}
})();
写完之后,使用webpack进行打包,导出一个经过压缩后的min.js版本供使用
使用
引入:
<script src="../dist/LazyImage.min.js"></script>
也支持commonjs和es6module规范
在需要加载的 img 标签上添加 'lazy-image' (默认,可自行修改)属性。然后将 img 标签的 opacity 设置为0,可根据需要添加默认的占位符。执行这个方法 LazyImage() 就可以把页面中需要延迟加载的图片做延迟加载。
支持自定义配置:
context:document指定上下文attr:'lazy-image'具备哪个属性的img需要做延迟加载(属性值是真实图片地址)threshold:1何时出现在视口中再出发加载1代表完全,0代表刚出现speed:300出现真实图片动画的过渡时间callback:Function.prototype图片加载成功后触发的回调函数
支持的方法:
refresh():对所有新加入的图片进行重新的懒加载设置
例如:
const lz = LazyImage({
threshold:0.5,
context:box
});
//新加入图片dom后
lz.refresh()
例子
可以去仓库 example文件夹下查看例子
对所有图片进行懒加载设置,并且滚动到底部后重新加入图片,并对新的图片进行懒加载设置
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>图片懒加载使用例子</title>
<style>
.wrapper {
width: 236px;
height: 420px;
margin: 0 auto;
background: url(./images/default.gif) center center no-repeat;
padding-bottom: 300px;
display: flex;
justify-content: center;
align-items: center;
}
.wrapper img {
opacity: 0;
}
</style>
</head>
<body>
<div class="img-area">
<div class="wrapper">
<img src="" alt="" lazy-image="./images/1.jpg">
</div>
<div class="wrapper">
<img src="" alt="" lazy-image="./images/1.jpg">
</div>
<div class="wrapper">
<img src="" alt="" lazy-image="./images/1.jpg">
</div>
</div>
<div id="bottom">
bottom
</div>
<script src="../dist/LazyImage.min.js"></script>
<script>
const imgArea = document.querySelector('.img-area')
//懒加载使用:
const lz = LazyImage({
threshold: 0.5,
speed: 1000,
callback: function (target) {
console.log(this, target)
}
})
//滚动到底部加载更多,并且依然对新增的div进行懒加载处理
const bottomOb = new IntersectionObserver((changes) => {
const {isIntersecting, target} = changes[0]
if(isIntersecting){
console.log('滚动到底部了,加载更多')
const div = document.createElement('div')
div.classList.add('wrapper')
div.innerHTML = `<img src="" alt="" lazy-image="./images/1.jpg">`
imgArea.appendChild(div)
lz.refresh()//对新加的DOM进行懒加载处理
}
}, {
threshold: [0]
})
bottomOb.observe(document.querySelector('#bottom'))
</script>
</body>
</html>