前言
在圖片應用較為頻繁的專案(官網,商城,桌面桌布專案等)中,如果我們單純地給每個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:這裡為了除錯方便我都用了同一個圖片地址,小夥伴們可以修改程式碼,用不同的圖片地址,自行除錯哦