基於react的lazy-load懶載入實現

拉風小牡丹發表於2019-04-17

前言

在圖片應用較為頻繁的專案(官網,商城,桌面桌布專案等)中,如果我們單純地給每個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>
複製程式碼

效果就是,在我們點選這個綠色區域時,會列印出這些引數

  1. window.innerHeight即為瀏覽器可視區域的高度
  2. node.getBoundingClientRect()方法執行返回了一個ClientRect物件,包含了鈣該元素的一些位置資訊

基於react的lazy-load懶載入實現
所以我們在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按鈕,回撥方法呼叫,控制檯列印引數

點選removeListener按鈕後再點選addChild按鈕,回撥方法不執行,控制檯也沒有引數列印

基於react的lazy-load懶載入實現

有興趣的同學可以瞭解一下MutationObserver的相關概念,該屬性的相容性如下,如果要相容IE11以下的情況,建議使用其他方法,比如輪詢,來代替這個api的使用

基於react的lazy-load懶載入實現

開幹

建立一個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:這裡為了除錯方便我都用了同一個圖片地址,小夥伴們可以修改程式碼,用不同的圖片地址,自行除錯哦

大功告成

相關文章