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>
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分析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]);
当元素可见比例超过指定阈值后,会调用一个回调函数,此回调函数接受两个参数:
entries
一个IntersectionObserverEntry对象列表(list),每个被触发的阈值,都会较指定阈值有偏差(或超出或少于指定阈值)。
observer
被调用的IntersectionObserver实例。
一个可以用来配置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.
_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;
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"];
目标元素与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);
};
清除定时器,取消延迟执行
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);
});
},