preface

In projects where images are frequently used (official website, shopping mall, desktop wallpaper project, etc.), if we simply attach SRC tags to each IMG tag or add background-image values to DOM nodes as the real address of the image, we can imagine that the browser will need to download all image resources, which occupies a considerable amount of network speed. This makes our web pages load very slowly.

Therefore, one of the solutions to this problem, lazy-load, came into being.

Train of thought

Listen for a scroll event that tells the browser to download the image resource when it scrolls to the image location

How to tell the browser to download the image resource, we need to save a real image path, put it in an attribute of the DOM node, and when we scroll to the image location, put the path in the SRC of the IMG tag or the background-image attribute of the div tag

Knowledge reserves

The dom node native method getBoundingClientRect

Write a pure HTML file to understand the method

<! 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>Copy the code

The effect is that when we click on this green area, these parameters are printed out

  1. Window. innerHeight is the height of the browser’s viewable area
  2. Node. GetBoundingClientRect execution () method returns a ClientRect object, included the location information of the element calcium

The native constructor MutationObserver listens for changes to the DOM of a DOM nodal node

The reason we need to know this is because, in a project, if there are a lot of images, we add them dynamically using pull-up load, pull-down refresh, etc. At this point, in order to ensure that lazy loading continues to use, we need to listen for the child node change event caused by the dynamic addition of pictures to do processing.

<! 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> <! -- Set public variables --> <script type = 'text/javascript'> window.father = document.getelementbyid ('father'); window.mutationObserver = undefined; </script> <! Add child node to parent node manually, check listener, Function addChild(){let father = window.father; let div = document.createElement('div'); Div. InnerHTML = '${math.random ()} (${window.mutationObserver? 'listener' : 'listener '})'; 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 }); } / / removing function added to the parent node event listeners removeListener () {window. MutationObserver & & window. MutationObserver. Disconnect && (typeof window.mutationObserver.disconnect === 'function') && window.mutationObserver.disconnect(); } </script> </html>Copy the code

The effect is that when the addChild button is clicked, the child element is added

Click the addListener button, then click the addChild button, callback the method call, and the console prints the parameters

If you click the removeListener button and then click the addChild button, the callback method is not executed and no arguments are printed on the console

If you are interested in MutationObserver, the compatibility of this property is as follows. If you want to be compatible with IE11, it is recommended to use other methods, such as polling, instead of using this API

Open dry

Create a React class

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;
Copy the code

Parameters in state

  • ImgList will store lazily loaded DOM nodes with image attributes
  • A mutationObserver listens for changes in the children of a parent node
  • Props External props (for details, see Initialization and Parameter Reception)

Receives four incoming parameters

  • FatherRef is used as the ref of the parent node
  • ClassName user-defined className
  • Style Indicates a custom style
  • The link tag stores the real address property name (using the data-* property)

Initialization and parameter reception

componentDidMount(){
	this.setState({ props : this.props }, () => this.init());
}

componentWillReceiveProps(nextProps){
	this.setState({ props : nextProps }, () => this.init());
}
Copy the code

When it comes to asynchronous operations, the received parameters are stored in state, and all the parameters in state are called in the component to facilitate the influence of the life cycle on parameter changes

Since the React version is not up to date at the time of testing, you are free to switch to the new API

Write the this.init method

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(); })}Copy the code

This method adds a listener event that listens for child node changes to call the image load event

And starts to initialize the load event that executes the image

Execute the image load event

/ / loaded with pictures startRenderImg () {window. The removeEventListener (' scroll ', enclosing imgRender); let { fatherRef } = this.state.props; let fatherNode = this.refs[fatherRef]; let childrenNodes = fatherNode && fatherNode.childNodes; // This. SetState ({imgList: {imgList:}); This.getimgtag (childrenNodes)}, () => {// Initialize render image this.imgrender (); // addScroll listener this.addscroll (); }); AddScroll (){let {fatherRef} = this.state.props; if(fatherRef){ this.refs[fatherRef].addEventListener('scroll', this.imgRender) }else{ window.addEventListener('scroll', This.imgrender)}} // Set imgList getImgTag(childrenNodes, imgList = []){let {link} = this.state.props; if(childrenNodes && childrenNodes.length > 0){ for(let i = 0 ; i < childrenNodes.length ; 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); }}} // return the dom node array with all {link} tags IsImgLoad} / / picture is in line with the loading conditions (node) {/ / pictures from the top of the distance < = the height of the browser visualization, illustrate the need for virtual SRC and real SRC replaced the let bound = node. GetBoundingClientRect ();  let clientHeight = window.innerHeight; return bound.top <= clientHeight; } imgLoad(index, node){let {imgList} = this.state; let { link } = this.state.props; If (node.tagname.tolowerCase () === 'img'){// If the img tag is assigned to SRC node.src = node.getAttribute(link); } else {/ / the rest status assigned to background figure node. Style. The backgroundImage = ` url (${node. The getAttribute (link)}) `; } // Delete the dom node 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]) } }Copy the code

Component code cleanup

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(); })} / / loaded with pictures startRenderImg () {window. The removeEventListener (' scroll ', enclosing imgRender); let { fatherRef } = this.state.props; let fatherNode = this.refs[fatherRef]; let childrenNodes = fatherNode && fatherNode.childNodes; // This. SetState ({imgList: {imgList:}); This.getimgtag (childrenNodes)}, () => {// Initialize render image this.imgrender (); // addScroll listener this.addscroll (); }); AddScroll (){let {fatherRef} = this.state.props; if(fatherRef){ this.refs[fatherRef].addEventListener('scroll', this.imgRender) }else{ window.addEventListener('scroll', This.imgrender)}} // Set imgList getImgTag(childrenNodes, imgList = []){let {link} = this.state.props; if(childrenNodes && childrenNodes.length > 0){ for(let i = 0 ; i < childrenNodes.length ; 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); }}} // return the dom node array with all {link} tags IsImgLoad} / / picture is in line with the loading conditions (node) {/ / pictures from the top of the distance < = the height of the browser visualization, illustrate the need for virtual SRC and real SRC replaced the let bound = node. GetBoundingClientRect ();  let clientHeight = window.innerHeight; return bound.top <= clientHeight; } imgLoad(index, node){let {imgList} = this.state; let { link } = this.state.props; If (node.tagname.tolowerCase () === 'img'){// If the img tag is assigned to SRC node.src = node.getAttribute(link); } else {/ / the rest status assigned to background figure node. Style. The backgroundImage = ` url (${node. The getAttribute (link)}) `; } // Delete the dom node 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;Copy the code

Business code practice

/* * * @state * imgSrc String Image URL * imgList Array Number of image arrays * fatherId String Single id of the parent node * Link String Native label name to store */ 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> {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})}}> Add </button> </div >)}} export default Test;Copy the code

After calling the Test method, open f12 pointing to the image DOM node

If you slide the scroll bar, you’ll find that the scroll bar rolls to a certain position

If the current DOM node is an IMG node, the SRC attribute is added. The current div node adds the backgroundImage property

Ps: I used the same picture address for debugging. You can modify the code and use different picture addresses to debug by yourself

You’re done