Skip to content

Latest commit

 

History

History
336 lines (280 loc) · 12.2 KB

8、图片懒加载.md

File metadata and controls

336 lines (280 loc) · 12.2 KB

Recommend.tsx

import LazyImage from 'reuse/lazyimg/Lazy-img'
...
<div className="recommend">
                    <Scroll scrollStyle="recommend-content">
                        <div>
                            <div className="slider-wrapper">
								...
                            </div>
                            <div className="recommend-list">
                                <h1 className="list-title">热门歌单推荐</h1>
                                <ul>
                                    {
                                        !!discList.length && discList.map((item, index)=>(
                                            <li className="item" key={index}>
                                                <div className="icon">
                                                    <LazyImage
                                                        sizes="200px"
                                                        src="https://placehold.it/200x300?text=Image1"
                                                        srcset={item.imgurl}
                                                        width="60"
                                                        height="60"
                                                    />
                                                </div>
                                                <div className="text">
                                                    <h2 className="name">{item.creator.name}</h2>
                                                    <p className="desc">{item.dissname}</p>
                                                </div>
                                            </li>
                                        ))
                                    }
                                </ul>
                            </div>
                        </div>
                    </Scroll>
            </div>

Lazy-img.tsx

import React,{Component} from "react";
import LazyLoad from "common/js/lazyload.es2015.js"
import logo from "./[email protected]"

interface LazyImageProps{
    alt?:string,
    src?:string,
    srcset?:string,
    sizes?:string,
    width?:string,
    height?:string
}

interface LazyImageState{

}

export class LazyImage extends Component <LazyImageProps, LazyImageState> {
    lazyLoadInstance:any
    constructor(props:LazyImageProps){
        super(props);
        this.lazyLoadInstance = null;
    }

    componentDidMount() {
        // console.log('lazyLoadInstance')
        console.log(logo)
        if(!this.lazyLoadInstance){
            const lazyloadConfig = {
                elements_selector: ".lazy",
                container: document.getElementsByClassName('recommend')[0],
                threshold:0
            };
            this.lazyLoadInstance = new LazyLoad(lazyloadConfig);
        }
        this.lazyLoadInstance.update();
    }
    render() {
        const { alt, srcset, sizes, width, height } = this.props;
        let src = this.props.src ? this.props.src : logo;
        return (
            <img
                alt={alt}
                className="lazy"
                src={logo}
                data-src={src}
                data-srcset={srcset}
                data-sizes={sizes}
                width={width}
                height={height}
            />
        );
    }
}

export default LazyImage;

这里将"vanilla-lazyload": "^8.17.0"中的lazyload.es2015.js文件作为公用的js工具代码,不直接引入npm包,npm包会出现刷新不成功的情况。

lazyload.es2015.js

lazyload.es2015.js分析vanilla-lazyload的图片懒加载原理。

在父组件componentDidMount中实例化LazyLoad,如下所示:

    const lazyloadConfig = {
        elements_selector: ".lazy",
        container: document.getElementsByClassName('recommend')[0],
        threshold:0
    };
    this.lazyLoadInstance = new LazyLoad(lazyloadConfig);

Lazyload构造函数与原型如下:

const LazyLoad = function(customSettings, elements) {
	//将用户的设置与默认的设置合并
	this._settings = getInstanceSettings(customSettings);
	//创建IntersectionObserver实例,设置事件回调函数
	this._setObserver();
	//loading的数量
	this._loadingCount = 0;
	//开始对目标元素监听
	this.update(elements);
};

LazyLoad.prototype = {
	_manageIntersection: function(entry) {},
	_onIntersection: function(entries) {},
	//监听器
	_setObserver: function() {},
	_updateLoadingCount: function(plusMinus) {},
	update: function(elements) {},
	destroy: function() {},
	load: function(element, force) {},
	loadAll: function() {}
};

预备知识:

IntersectionObserver,IntersectionObserver接口(从属于Intersection Observer API)为开发者提供了一种可以异步监听目标元素与其祖先或视窗(viewport)交叉状态的手段。祖先元素与视窗(viewport)被称为根(root)。

var observer = new IntersectionObserver(callback[, options]);

callback

当元素可见比例超过指定阈值后,会调用一个回调函数,此回调函数接受两个参数:

entries

一个IntersectionObserverEntry对象列表(list),每个被触发的阈值,都会较指定阈值有偏差(或超出或少于指定阈值)。

observer

被调用的IntersectionObserver实例。

options 可选

一个可以用来配置observer实例的对象。如果options未指定,observer实例默认使用viewport作为根(root),并且没有margin, 阈值为0% (意味着即使一像素的改变都会触发回调函数)。你可以指定以下配置:

root

监听元素的祖先元素Element对象,其边界盒将被视作viewport。目标在根的可见区域的的任何不可见部分都会被视为不可见。

rootMargin

一个在计算交叉值时添加至根的边界盒(bounding_box)中的一组偏移量,类型为字字符串(string) ,可以有效的缩小或扩大根的判定范围从而满足计算需要。语法大致和CSS 中的margin 属性等同; 可以参考 The root element and root margin in Intersection Observer API来深入了解margin的工作原理及其语法。默认值是"0px 0px 0px 0px".

threshold

规定了一个监听目标与边界盒交叉区域的比例值,可以是一个具体的数值或是一组0.0到1.0之间的数组。若指定值为0.0,则意味着监听元素即使与根有1像素交叉,此元素也会被视为可见. 若指定值为1.0,则意味着整个元素都是可见的 。可以参考Thresholds in Intersection Observer API 来深入了解阈值是如何使用的。阈值的默认值为0.0.

创建IntersectionObserver实例,设置事件回调函数,回调函数的作用是加载资源

_setObserver: function() {
	//判断浏览器是否支持IntersectionObserver
	if (!supportsIntersectionObserver) {
		return;
	}
	//
	this._observer = new IntersectionObserver(
		this._onIntersection.bind(this),
		getObserverSettings(this._settings)
	);
},

const getObserverSettings = settings => ({
	root: settings.container === document ? null : settings.container,
	rootMargin: settings.thresholds || settings.threshold + "px"
});

_onIntersection: function(entries) {
	entries.forEach(this._manageIntersection.bind(this));
},

_manageIntersection: function(entry) {
	var observer = this._observer;
	var loadDelay = this._settings.load_delay;
	var element = entry.target;

	// WITHOUT LOAD DELAY
	// 判断用户是否设置了延迟加载的时间,即停留在可视窗口的一定时间之后再加载图片
	if (!loadDelay) {

		if (isIntersecting(entry)) {
			loadAndUnobserve(element, observer, this);
		}
		return;
	}

	// WITH LOAD DELAY
	if (isIntersecting(entry)) {
		delayLoad(element, observer, this);
	} else {
		cancelDelayLoad(element);
	}
},

const isIntersecting = entry =>
	entry.isIntersecting || entry.intersectionRatio > 0;

未设置load_delay

const loadAndUnobserve = (element, observer, instance) => {
	revealElement(element, instance);
	observer.unobserve(element);
};


function revealElement(element, instance, force) {
	var settings = instance._settings;
	//判断不需要强制加载并且该元素已经加载完成,则不需要做任何操作
	if (!force && getWasProcessedData(element)) {
		return; // element has already been processed and force wasn't true
	}
	//执行用户给的回调函数:settings.callback_enter(element)
	callbackIfSet(settings.callback_enter, element);
	if (managedTags.indexOf(element.tagName) > -1) {
		//设置自定义事件,当element的load事件触发之后将loading状态的class替换成loaded
		addOneShotEventListeners(element, instance);
		//设置element的class为loading,表示正在加载
		addClass(element, settings.class_loading);
	}
	//主要的加载逻辑在setSources函数中,如果是img则调用setSourcesImg函数,setSourcesImg函数对图片作了优化,选用WebP的文件格式进行优化。
	setSources(element, instance);
	//在元素上设置属性data-wap-processed为true
	setWasProcessedData(element);
	callbackIfSet(settings.callback_set, element);
}

const managedTags = ["IMG", "IFRAME", "VIDEO"];

设置了load_delay

目标元素与root交叉

const delayLoad = (element, observer, instance) => {
	var loadDelay = instance._settings.load_delay;
	var timeoutId = getTimeoutData(element);
	if (timeoutId) {
		return; // do nothing if timeout already set
	}
	timeoutId = setTimeout(function() {
		loadAndUnobserve(element, observer, instance);
		//清除定时器,取消延迟执行
		cancelDelayLoad(element);
	}, loadDelay);
	//将定时器id保存到元素的自定义属性上。
	setTimeoutData(element, timeoutId);
};

设置一个定时器,用于延迟执行

目标元素未与root交叉

const cancelDelayLoad = element => {
	var timeoutId = getTimeoutData(element);
	if (!timeoutId) {
		return; // do nothing if timeout doesn't exist
	}
	clearTimeout(timeoutId);
	setTimeoutData(element, null);
};

清除定时器,取消延迟执行

回调函数中起到加载资源的部分为setSources函数

const setSources = (element, instance) => {
	const settings = instance._settings;
	const tagName = element.tagName;
	const setSourcesFunction = setSourcesFunctions[tagName];
	if (setSourcesFunction) {
		setSourcesFunction(element, settings);
		instance._updateLoadingCount(1);
		//处理一个元素之后,从即将被处理的元素集合中删除
		instance._elements = purgeOneElement(instance._elements, element);
		return;
	}
	setSourcesBgImage(element, settings);
};

const setSourcesImg = (element, settings) => {
	const toWebpFlag = supportsWebp && settings.to_webp;
	const srcsetDataName = settings.data_srcset;
	const parent = element.parentNode;

	if (parent && parent.tagName === "PICTURE") {
		setSourcesInChildren(parent, "srcset", srcsetDataName, toWebpFlag);
	}
	//给img元素上的sizes,srcset,src赋值
	const sizesDataValue = getData(element, settings.data_sizes);
	setAttributeIfValue(element, "sizes", sizesDataValue);
	const srcsetDataValue = getData(element, srcsetDataName);
	setAttributeIfValue(element, "srcset", srcsetDataValue, toWebpFlag);
	const srcDataValue = getData(element, settings.data_src);
	setAttributeIfValue(element, "src", srcDataValue, toWebpFlag);
};

开始对目标元素监听

执行update函数利用IntersectionObserver创建的监听器对目标元素进行监听。

update: function(elements) {
	const settings = this._settings;
	const nodeSet =
		elements ||
		settings.container.querySelectorAll(settings.elements_selector);

	//获取没有被处理的元素集合
	this._elements = purgeProcessedElements(
		Array.prototype.slice.call(nodeSet) // NOTE: nodeset to array for IE compatibility
	);

	if (isBot || !this._observer) {
		this.loadAll();
		return;
	}

    //每个元素都用IntersectionObserver创建的监听器进行监听,当img元素与root交叉时,执行设置的回调函数。
	this._elements.forEach(element => {
		this._observer.observe(element);
	});
},