基於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:這裏爲了調試方便我都用了同一個圖片地址,小夥伴們能夠修改代碼,用不一樣的圖片地址,自行調試哦

大功告成

相關文章
相關標籤/搜索