基于react的lazy-load懒加载实现

前言

在图片应用较为频繁的项目(官网,商城,桌面壁纸项目等)中,若是咱们单纯地给每一个img标签附加src标签或者给dom节点添加background-image赋值为图片真实地址的话,可想而知浏览器是须要下载全部的图片资源,至关占用网速,这使得咱们的网页加载的十分缓慢。javascript

因而,关于解决这种问题的方案之一,lazy-load,懒加载思想应运而生。html

思路

监听滚动事件,当滚动到该图片所在的位置的时候,告知浏览器下载此图片资源java

如何告知浏览器下载图片资源,咱们须要存出一个真实图片路径,放在dom节点的某个属性中,等到真正滚动到该图片位置的时候,将路径放到img标签的src中或者div等标签的background-image属性中node

知识储备

dom节点原生方法getBoundingClientRect

写一个纯粹一点的html文件来了解该方法react

<!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>
复制代码

效果就是,在咱们点击这个绿色区域时,会打印出这些参数api

  1. window.innerHeight即为浏览器可视区域的高度
  2. node.getBoundingClientRect()方法执行返回了一个ClientRect对象,包含了钙该元素的一些位置信息

因此咱们在lazy-load中判断图片是否到达可视区域的方法,就用这两个参数来比对

监听一个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按钮,回调方法调用,控制台打印参数app

点击removeListener按钮后再点击addChild按钮,回调方法不执行,控制台也没有参数打印dom

有兴趣的同窗能够了解一下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:这里为了调试方便我都用了同一个图片地址,小伙伴们能够修改代码,用不一样的图片地址,自行调试哦

大功告成

相关文章
相关标签/搜索