前言
早在多年前,lazyload 已经出现了,懒加载在前端里边同样具有十分重要的意义。react-lazyload 的作用是当组件未出现在屏幕内时,不去挂载该组件,而是使用 placeholder 去渲染,让滚动使内容出现后,组件会被挂载。就是这么简单!例如,一个复杂的组件(非首屏内容),使用了懒加载后,渲染首屏就会节省很多资源,从而减少首屏渲染时间。
Demo
源码地址 react-lazyload
Demo地址 Demo
HelloWorld
将需要懒加载的组件使用 LazyLoad 包裹即可,最好使用 height 进行站位,否则该组件位置将会为 0
/*
Lazy loading images is supported out of box,
no extra config needed, set `height` for better
experience
*/
解析
从源码角度分析~
一览核心
本小节摘取了最核心的代码,目的在于对 LazyLoad 组件有个最核心的认识,它的核心就是监听滚动事件,检查组件是否在屏幕内,如果在的话就显示,不在的话就不显示~
class LazyLoad extends Component {
componentDidMount() {
on(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent);
}
render() {
return this.visible ?
this.props.children :
this.props.placeholder ?
this.props.placeholder :
;
}
}
LazyLoad 的属性,透过属性,我们可以知道它大概有些什么功能。
LazyLoad.propTypes = {
once: PropTypes.bool,
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
offset: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]),
overflow: PropTypes.bool, // 不是 window 滚动,而使用了 overflow: scroll
resize: PropTypes.bool, // 是否监听 resize
scroll: PropTypes.bool, // 是否监听滚动
children: PropTypes.node,
throttle: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
debounce: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
placeholder: PropTypes.node,
scrollContainer: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
unmountIfInvisible: PropTypes.bool,
preventLoading: PropTypes.bool
};
// 默认值
LazyLoad.defaultProps = {
once: false,
offset: 0,
overflow: false,
resize: false,
scroll: true,
unmountIfInvisible: false,
preventLoading: false,
};
完整的 componentDidMount,scrollport 是滚动试图,默认是 window,如果 props 传入了 scrollContainer,那么滚动试图将是自定义的。needResetFinalLazyLoadHandler 是控制是否重置滚动监听。debounce 和 throttle 分别是用来控制滚动事件的监听触发频率,默认都是 undefine,needResetFinalLazyLoadHandler 初始值为 false。finalLazyLoadHandler 初始值也为 undefine,而 overflow 也为 false,scroll 为 true,listeners 是需要懒加载的组件集合,初始大小肯定为0,componentDidMount 最后才会进行添加,因此最终会走到 **on(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent),事件只需要一次绑定即可。
**
componentDidMount() {
// It's unlikely to change delay type on the fly, this is mainly
// designed for tests
let scrollport = window;
const {
scrollContainer,
} = this.props;
if (scrollContainer) {
if (isString(scrollContainer)) {
scrollport = scrollport.document.querySelector(scrollContainer);
}
}
const needResetFinalLazyLoadHandler = (this.props.debounce !== undefined && delayType === 'throttle')
|| (delayType === 'debounce' && this.props.debounce === undefined);
if (needResetFinalLazyLoadHandler) {
off(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent);
off(window, 'resize', finalLazyLoadHandler, passiveEvent);
finalLazyLoadHandler = null;
}
if (!finalLazyLoadHandler) {
if (this.props.debounce !== undefined) {
finalLazyLoadHandler = debounce(lazyLoadHandler, typeof this.props.debounce === 'number' ?
this.props.debounce :
300);
delayType = 'debounce';
} else if (this.props.throttle !== undefined) {
finalLazyLoadHandler = throttle(lazyLoadHandler, typeof this.props.throttle === 'number' ?
this.props.throttle :
300);
delayType = 'throttle';
} else {
finalLazyLoadHandler = lazyLoadHandler;
}
}
if (this.props.overflow) {
const parent = scrollParent(this.ref);
if (parent && typeof parent.getAttribute === 'function') {
const listenerCount = 1 + (+parent.getAttribute(LISTEN_FLAG));
if (listenerCount === 1) {
parent.addEventListener('scroll', finalLazyLoadHandler, passiveEvent);
}
parent.setAttribute(LISTEN_FLAG, listenerCount);
}
} else if (listeners.length === 0 || needResetFinalLazyLoadHandler) {
const { scroll, resize } = this.props;
if (scroll) {
on(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent);
}
if (resize) {
on(window, 'resize', finalLazyLoadHandler, passiveEvent);
}
}
listeners.push(this);
checkVisible(this);
}
通常 finalLazyLoadHandler 就是 lazyLoadHandler,不会对滚动事件进行 debounce 或 throttle,我们一般为了性能,会使用 throttle 进行处理。函数会对每一个懒加载组件进行 checkVisible,之后会移除 once component
const lazyLoadHandler = () => {
for (let i = 0; i < listeners.length; ++i) {
const listener = listeners[i];
checkVisible(listener);
}
// Remove `once` component in listeners
purgePending();
};
checkVisible,检查组件是否出现在 viewport 中,如果出现了就吧 visible 设置为 true,当然如果设置了 unmountIfInvisible = true,那么不可见时组件将被移除,如果之前已经渲染了,需要避免再次渲染。
const checkVisible = function checkVisible(component) {
const node = component.ref;
if (!(node instanceof HTMLElement)) {
return;
}
const parent = scrollParent(node);
const isOverflow = component.props.overflow &&
parent !== node.ownerDocument &&
parent !== document &&
parent !== document.documentElement;
const visible = isOverflow ?
checkOverflowVisible(component, parent) :
checkNormalVisible(component);
if (visible) {
// Avoid extra render if previously is visible
if (!component.visible && !component.preventLoading) {
if (component.props.once) {
pending.push(component);
}
component.visible = true;
component.forceUpdate();
}
} else if (!(component.props.once && component.visible)) {
component.visible = false;
if (component.props.unmountIfInvisible) {
component.forceUpdate();
}
}
};
checkNormalVisible 检查组件是否 visible 的函数,判断组件的getgetBoundingClientRect 的 top - offset(相对于屏幕顶部的距离) 与 window 的 height 之间的关系
const checkNormalVisible = function checkNormalVisible(component) {
const node = component.ref;
// If this element is hidden by css rules somehow, it's definitely invisible
if (!(node.offsetWidth || node.offsetHeight || node.getClientRects().length)) return false;
let top;
let elementHeight;
try {
// 这个语法 node 也是支持的
({ top, height: elementHeight } = node.getBoundingClientRect());
} catch (e) {
({ top, height: elementHeight } = defaultBoundingClientRect);
}
const windowInnerHeight = window.innerHeight || document.documentElement.clientHeight;
const offsets = Array.isArray(component.props.offset) ?
component.props.offset :
[component.props.offset, component.props.offset]; // Be compatible with previous API
return (top - offsets[0] <= windowInnerHeight) &&
(top + elementHeight + offsets[1] >= 0);
};
(top - offsets[0] <= windowInnerHeight) &&
(top + elementHeight + offsets[1] >= 0);
一张图解析!
到这里解析的差不多了
欣赏一下 throttle
export default function throttle(fn, threshhold, scope) {
threshhold || (threshhold = 250);
var last,
deferTimer;
return function () {
var context = scope || this;
var now = +new Date,
args = arguments;
if (last && now < last + threshhold) {
// hold on to it
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
fn.apply(context, args);
}, threshhold);
} else {
last = now;
fn.apply(context, args);
}
};
}
再欣赏一下 debounce
export default function debounce(func, wait, immediate) {
let timeout;
let args;
let context;
let timestamp;
let result;
const later = function later() {
const last = +(new Date()) - timestamp;
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) {
context = null;
args = null;
}
}
}
};
return function debounced() {
context = this;
args = arguments;
timestamp = +(new Date());
const callNow = immediate && !timeout;
if (!timeout) {
timeout = setTimeout(later, wait);
}
if (callNow) {
result = func.apply(context, args);
context = null;
args = null;
}
return result;
};
}
获取 scrollParent
export default (node) => {
if (!(node instanceof HTMLElement)) {
return document.documentElement;
}
const excludeStaticParent = node.style.position === 'absolute';
const overflowRegex = /(scroll|auto)/;
let parent = node;
while (parent) {
if (!parent.parentNode) {
return node.ownerDocument || document.documentElement;
}
const style = window.getComputedStyle(parent);
const position = style.position;
const overflow = style.overflow;
const overflowX = style['overflow-x'];
const overflowY = style['overflow-y'];
if (position === 'static' && excludeStaticParent) {
parent = parent.parentNode;
continue;
}
if (overflowRegex.test(overflow) && overflowRegex.test(overflowX) && overflowRegex.test(overflowY)) {
return parent;
}
parent = parent.parentNode;
}
return node.ownerDocument || node.documentElement || document.documentElement;
};
总结思考
我们可以看到,Lazyload 并不能实现类似客户端的图片懒加载,Lazyload 加载图片也会出现白屏时间,解决办法是使用 image.onload,当图片资源请求关闭后,再显示图片,就可以做到类似客户端的效果。