前言
在图片应用较为频繁的项目(官网,商城,桌面壁纸项目等)中,如果我们单纯地给每个img标签附加src标签或者给dom节点添加background-image赋值为图片真实地址的话,可想而知浏览器是需要下载所有的图片资源,相当占用网速,这使得我们的网页加载的十分缓慢。
于是,关于解决这种问题的方案之一,lazy-load,懒加载思想应运而生。
思路
监听滚动事件,当滚动到该图片所在的位置的时候,告知浏览器下载此图片资源
如何告知浏览器下载图片资源,我们需要存出一个真实图片路径,放在dom节点的某个属性中,等到真正滚动到该图片位置的时候,将路径放到img标签的src中或者div等标签的background-image属性中
知识储备
dom节点原生方法getBoundingClientRect
写一个纯粹一点的html文件来了解该方法
<!doctype html>
<html>
<head>
<meta charset = "utf-8">
<style>
html, body{
margin : 0;
padding : 0;
}
body{
position : relative;
}
div{
position : absolute;
top : 50px;
left : 100px;
height : 50px;
width : 50px;
background : #5d9;
cursor : pointer;
}
</style>
</head>
<body>
<div onclick = "getPos(this)"></div>
</body>
<script type = 'text/javascript'>
function getPos(node){
console.info(window.innerHeight)
console.info(node.getBoundingClientRect())
}
</script>
</html>
复制代码
效果就是,在我们点击这个绿色区域时,会打印出这些参数
- window.innerHeight即为浏览器可视区域的高度
- node.getBoundingClientRect()方法执行返回了一个ClientRect对象,包含了钙该元素的一些位置信息
监听一个dom节点子节点dom发生改变的原生构造函数MutationObserver
我们需要了解这个的原因是因为,在项目中,如果图片非常多,我们会采用上拉加载下拉刷新等功能动态添加图片。此时我们为了能保证懒加载继续使用,就需要监听因为图片动态添加造成的子节点改变事件来做处理。
<!doctype html>
<html>
<head>
<meta charset = 'urf-8'/>
</head>
<body>
<button onclick = 'addChild()'>addChild</button>
<button onclick = 'addListener()'>addListener</button>
<button onclick = 'removeListener()'>removeListener</button>
<div id = 'father'></div>
</body>
<!-- 设置公共变量 -->
<script type = 'text/javascript'>
window.father = document.getElementById('father');
window.mutationObserver = undefined;
</script>
<!-- 手动给父节点添加子节点,校验监听,移除监听 -->
<script type = 'text/javascript'>
//给父节点添加子节点事件
function addChild(){
let father = window.father;
let div = document.createElement('div');
div.innerHTML = `${Math.random()}(${window.mutationObserver ? '有监听' : '无监听'})`;
father.appendChild(div);
}
//监听给父节点添加子节点事件
function addListener(){
if(window.mutationObserver){
removeListener();
}
window.mutationObserver = new MutationObserver((...rest) => { console.info(rest) });
mutationObserver.observe(window.father, { childList : true , attributes : true , characterData : true });
}
//移除给父节点添加子节点事件监听
function removeListener(){
window.mutationObserver && window.mutationObserver.disconnect && (typeof window.mutationObserver.disconnect === 'function') && window.mutationObserver.disconnect();
}
</script>
</html>
复制代码
效果就是,在点击addChild按钮时,会添加子元素
点击addListener按钮后再点击addChild按钮,回调方法调用,控制台打印参数
点击removeListener按钮后再点击addChild按钮,回调方法不执行,控制台也没有参数打印
有兴趣的同学可以了解一下MutationObserver的相关概念,该属性的兼容性如下,如果要兼容IE11以下的情况,建议使用其他方法,比如轮询,来代替这个api的使用
开干
创建一个react类
class ReactLazyLoad extends React.Component{
constructor(props){
super(props);
this.state = {
imgList : [],
mutationObserver : undefined,
props : {}
}
this.imgRender = this.imgRender.bind(this);
}
render(){
let { fatherRef , children , style , className } = this.state.props;
return(
<div ref = { fatherRef } className = { className } style = { style }>
{ children }
</div>
)
}
}
ReactLazyLoad.defaultProps = {
fatherRef : 'fatherRef',
className : '',
style : {},
link : 'data-original'
}
export default ReactLazyLoad;
复制代码
state中的参数
- imgList 即将存储懒加载的有图片属性的dom节点
- mutationObserver 监听父节点内子节点变化的对象
- props 外部传入的props(具体作用见 初始化与参数接收)
接收4个入参
- fatherRef 用作父节点的ref
- className 自定义类名
- style 自定义样式
- link 标签中存真实地址的属性名(使用data-*属性)
初始化与参数接收
componentDidMount(){
this.setState({ props : this.props }, () => this.init());
}
componentWillReceiveProps(nextProps){
this.setState({ props : nextProps }, () => this.init());
}
复制代码
涉及到异步操作,这里把接收到的参数存入state中,在组件内调用全部调用state中的参数,方便生命周期对参数改变的影响
因为测试时react版本不是最新,各位可以灵活替换为新的api
编写this.init方法
init(){
let { mutationObserver } = this.state;
let { fatherRef } = this.state.props;
let fatherNode = this.refs[fatherRef];
mutationObserver && mutationObserver.disconnect && (typeof mutationObserver.disconnect === 'function') && mutationObserver.disconnect();
mutationObserver = new MutationObserver(() => this.startRenderImg());
this.setState({ mutationObserver }, () => {
mutationObserver.observe(fatherNode, { childList : true , attributes : true , characterData : true });
this.startRenderImg();
})
}
复制代码
这一个方法添加了监听子节点变化的监听事件来调用图片加载事件
并且开始初始化执行图片的加载事件
执行图片加载事件
//开始进行图片加载
startRenderImg(){
window.removeEventListener('scroll', this.imgRender);
let { fatherRef } = this.state.props;
let fatherNode = this.refs[fatherRef];
let childrenNodes = fatherNode && fatherNode.childNodes;
//通过原生操作获取所有的子节点中具有{link}属性的标签
this.setState({ imgList : this.getImgTag(childrenNodes) }, () => {
//初始化渲染图片
this.imgRender();
//添加滚动监听
this.addScroll();
});
}
//添加滚动监听
addScroll(){
let { fatherRef } = this.state.props;
if(fatherRef){
this.refs[fatherRef].addEventListener('scroll', this.imgRender)
}else{
window.addEventListener('scroll', this.imgRender)
}
}
//设置imgList
getImgTag(childrenNodes, imgList = []){
let { link } = this.state.props;
if(childrenNodes && childrenNodes.length > 0){
for(let i = 0 ; i < childrenNodes.length ; i++){
//只要是包含了{link}标签的元素 则放在渲染队列中
if(typeof(childrenNodes[i].getAttribute) === 'function' && childrenNodes[i].getAttribute(link)){
imgList.push(childrenNodes[i]);
}
//递归当前元素子元素
if(childrenNodes[i].childNodes && childrenNodes[i].childNodes.length > 0){
this.getImgTag(childrenNodes[i].childNodes, imgList);
}
}
}
//返回了具有所有{link}标签的dom节点数组
return imgList;
}
//图片是否符合加载条件
isImgLoad(node){
//图片距离顶部的距离 <= 浏览器可视化的高度,说明需要进行虚拟src与真实src的替换了
let bound = node.getBoundingClientRect();
let clientHeight = window.innerHeight;
return bound.top <= clientHeight;
}
//每一个图片的加载
imgLoad(index, node){
let { imgList } = this.state;
let { link } = this.state.props;
//获取之前设置好的{link}并且赋值给相应元素
if(node.tagName.toLowerCase() === 'img'){
//如果是img标签 则赋值给src
node.src = node.getAttribute(link);
}else{
//其余状况赋值给背景图
node.style.backgroundImage = `url(${node.getAttribute(link)})`;
}
//已加载了该图片,在资源数组中就删除该dom节点
imgList.splice(index, 1);
this.setState({ imgList });
}
//图片加载
imgRender(){
let { imgList } = this.state;
//因为加载后则删除已加载的元素,逆向遍历方便一些
for(let i = imgList.length - 1 ; i > -1 ; i--) {
this.isImgLoad(imgList[i]) && this.imgLoad(i, imgList[i])
}
}
复制代码
组件代码整理
class ReactLazyLoad extends React.Component{
constructor(props){
super(props);
this.state = {
imgList : [],
mutationObserver : undefined,
props : {}
}
this.imgRender = this.imgRender.bind(this);
}
componentDidMount(){
this.setState({ props : this.props }, () => this.init());
}
componentWillUnmount(){
window.removeEventListener('scroll', this.imgRender);
}
componentWillReceiveProps(nextProps){
this.setState({ props : nextProps }, () => this.init());
}
init(){
let { mutationObserver } = this.state;
let { fatherRef } = this.state.props;
let fatherNode = this.refs[fatherRef];
mutationObserver && mutationObserver.disconnect && (typeof mutationObserver.disconnect === 'function') && mutationObserver.disconnect();
mutationObserver = new MutationObserver(() => this.startRenderImg());
this.setState({ mutationObserver }, () => {
mutationObserver.observe(fatherNode, { childList : true , attributes : true , characterData : true });
this.startRenderImg();
})
}
//开始进行图片加载
startRenderImg(){
window.removeEventListener('scroll', this.imgRender);
let { fatherRef } = this.state.props;
let fatherNode = this.refs[fatherRef];
let childrenNodes = fatherNode && fatherNode.childNodes;
//通过原生操作获取所有的子节点中具有{link}属性的标签
this.setState({ imgList : this.getImgTag(childrenNodes) }, () => {
//初始化渲染图片
this.imgRender();
//添加滚动监听
this.addScroll();
});
}
//添加滚动监听
addScroll(){
let { fatherRef } = this.state.props;
if(fatherRef){
this.refs[fatherRef].addEventListener('scroll', this.imgRender)
}else{
window.addEventListener('scroll', this.imgRender)
}
}
//设置imgList
getImgTag(childrenNodes, imgList = []){
let { link } = this.state.props;
if(childrenNodes && childrenNodes.length > 0){
for(let i = 0 ; i < childrenNodes.length ; i++){
//只要是包含了{link}标签的元素 则放在渲染队列中
if(typeof(childrenNodes[i].getAttribute) === 'function' && childrenNodes[i].getAttribute(link)){
imgList.push(childrenNodes[i]);
}
//递归当前元素子元素
if(childrenNodes[i].childNodes && childrenNodes[i].childNodes.length > 0){
this.getImgTag(childrenNodes[i].childNodes, imgList);
}
}
}
//返回了具有所有{link}标签的dom节点数组
return imgList;
}
//图片是否符合加载条件
isImgLoad(node){
//图片距离顶部的距离 <= 浏览器可视化的高度,说明需要进行虚拟src与真实src的替换了
let bound = node.getBoundingClientRect();
let clientHeight = window.innerHeight;
return bound.top <= clientHeight;
}
//每一个图片的加载
imgLoad(index, node){
let { imgList } = this.state;
let { link } = this.state.props;
//获取之前设置好的{link}并且赋值给相应元素
if(node.tagName.toLowerCase() === 'img'){
//如果是img标签 则赋值给src
node.src = node.getAttribute(link);
}else{
//其余状况赋值给背景图
node.style.backgroundImage = `url(${node.getAttribute(link)})`;
}
//已加载了该图片,在资源数组中就删除该dom节点
imgList.splice(index, 1);
this.setState({ imgList });
}
//图片加载
imgRender(){
let { imgList } = this.state;
//因为加载后则删除已加载的元素,逆向遍历方便一些
for(let i = imgList.length - 1 ; i > -1 ; i--) {
this.isImgLoad(imgList[i]) && this.imgLoad(i, imgList[i])
}
}
render(){
let { fatherRef , children , style , className } = this.state.props;
return(
<div ref = { fatherRef } className = { className } style = { style }>
{ children }
</div>
)
}
}
ReactLazyLoad.defaultProps = {
fatherRef : 'fatherRef',
className : '',
style : {},
link : 'data-original'
}
export default ReactLazyLoad;
复制代码
业务代码实操
/* *
* @state
* imgSrc string 图片url地址
* imgList array 图片数组个数
* fatherId string 父节点单一标识
* link string 需要存储的原生标签名
*/
import React from 'react';
import ReactLazyLoad from './ReactLazyLoad';
class Test extends React.Component{
constructor(props){
super(props);
this.state = {
imgSrc : 'xxx',
imgList : Array(10).fill(),
fatherId : 'lazy-load-content',
link : 'data-original',
}
}
render(){
let { imgSrc , imgList , fatherId , link } = this.state;
return(
<div>
<ReactLazyLoad fatherRef = { fatherId } style = {{ width : '100%' , height : '400px' , overflow : 'auto' , border : '1px solid #ddd' }}>
{ imgArr && imgArr.length > 0 && imgArr.map((item, index) => {
let obj = { key : index , className : styles.img };
obj[link] = imgSrc;
return React.createElement('div', obj);
}) }
{ imgArr && imgArr.length > 0 && imgArr.map((item, index) => {
let obj = { key : index , className : styles.img };
obj[link] = imgSrc;
return React.createElement('img', obj);
}) }
<div>
这是混淆视听的部分
<div>
<div>这还是混淆视听的部分</div>
{ imgArr && imgArr.length > 0 && imgArr.map((item, index) => {
let obj = { key : index , className : styles.img };
obj[link] = imgSrc;
return React.createElement('img', obj);
}) }
</div>
</div>
</ReactLazyLoad>
<button onClick = {() => { imgArr.push(undefined); this.setState({ imgArr }) }}>添加</button>
</div >
)
}
}
export default Test;
复制代码
在调用Test方法之后,打开f12指到图片dom节点
滑动滚动条,会发现滚动条滚到一定的位置
当前dom节点如果是img节点,就会添加src属性;当前是div节点,则会添加backgroundImage属性
ps:这里为了调试方便我都用了同一个图片地址,小伙伴们可以修改代码,用不同的图片地址,自行调试哦