在图片应用较为频繁的项目(官网,商城,桌面壁纸项目等)中,若是咱们单纯地给每一个img标签附加src标签或者给dom节点添加background-image赋值为图片真实地址的话,可想而知浏览器是须要下载全部的图片资源,至关占用网速,这使得咱们的网页加载的十分缓慢。javascript
因而,关于解决这种问题的方案之一,lazy-load,懒加载思想应运而生。html
监听滚动事件,当滚动到该图片所在的位置的时候,告知浏览器下载此图片资源java
如何告知浏览器下载图片资源,咱们须要存出一个真实图片路径,放在dom节点的某个属性中,等到真正滚动到该图片位置的时候,将路径放到img标签的src中或者div等标签的background-image属性中node
写一个纯粹一点的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
咱们须要了解这个的缘由是由于,在项目中,若是图片很是多,咱们会采用上拉加载下拉刷新等功能动态添加图片。此时咱们为了能保证懒加载继续使用,就须要监听由于图片动态添加形成的子节点改变事件来作处理。数组
<!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的使用
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中的参数
接收4个入参
componentDidMount(){
this.setState({ props : this.props }, () => this.init());
}
componentWillReceiveProps(nextProps){
this.setState({ props : nextProps }, () => this.init());
}
复制代码
涉及到异步操做,这里把接收到的参数存入state中,在组件内调用所有调用state中的参数,方便生命周期对参数改变的影响
由于测试时react版本不是最新,各位能够灵活替换为新的api
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:这里为了调试方便我都用了同一个图片地址,小伙伴们能够修改代码,用不一样的图片地址,自行调试哦