本篇文章主要介绍一个优秀的基于react实现的懒加载控件:github.com/twobin/reac…。javascript
<Lazyload throttle={200} height={300}>
<img src="http://ww3.sinaimg.cn/mw690/62aad664jw1f2nxvya0u2j20u01hc16p.jpg" /> </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,
resize: PropTypes.bool,
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
};
复制代码
class LazyLoad extends Component {
constructor(props) {
super(props)
this.visible = false;
}
componentDidMount() {
...
}
shouldComponentUpdate() {
return this.visible;
}
componentWillUnmount() {
...
}
render() {
return this.visible ?
this.props.children :
this.props.placeholder ?
this.props.placeholder :
<div style={{ height: this.props.height }} className="lazyload-placeholder" />; } 复制代码
在通过一个简单猜想后,咱们仍是实际仍是应该一步步去看着代码,带着咱们以前“好奇”的问题,来近一步探寻这个精巧的懒加载组件是如何完成的。css
componentDidMount() {
// It's unlikely to change delay type on the fly, this is mainly
// designed for tests
let scrollport = window; // 这个地方不难理解,正常咱们懒加载滑动窗口都是window
const { // 设置完默认的scrollport,从正常需求来看,会存在滑动窗口并不是是window的状况,
scrollContainer, // 因此props上会暴露一个scrollContainer的api来处理这种状况
} = this.props;
if (scrollContainer) {
if (isString(scrollContainer)) {
scrollport = scrollport.document.querySelector(scrollContainer);
}
// TODO(疑问):若是scrollContainer是Object的状况呢?api是支持这个数据类型的
}
// 这里从变量名来看应该是判断是否是须要重载 debounce 或则 throttle的
// TODO(疑问),看起来这里彷佛有点费解,是否是有bug?
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;
}
}
// 这个overflow api从下面的逻辑看,应该是判断组件是否包含在非window对象的容器中的懒加载
if (this.props.overflow) {
// 若是是,就找到包含该组件的父及容器
const parent = scrollParent(ReactDom.findDOMNode(this));
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) {
// 从下面逻辑看,listeners数组是存储被懒加载的组件集合(单例)
// 结合以前的内容看(scrollport),这里是在对没传overflow参数时,事件绑定的处理
// TODO(疑问):这里是否是也应该用上面打标记计数的方式,标记一个容器只能被监听一次
const { scroll, resize } = this.props;
if (scroll) {
on(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent);
}
if (resize) {
on(window, 'resize', finalLazyLoadHandler, passiveEvent);
}
}
listeners.push(this);
// 此处应该是改变this.visible的地方,后面的细节3会详细讲解这部分逻辑
checkVisible(this);
}
复制代码
由下面代码能够看出,同一个容器内的scroll/resize事件监听只会进行一次,屡次的合并是经过listener数组作到的。那么这里也有一个疑问:当前的逻辑,彷佛没法知足当一个页面中有多个容器的懒加载时,每次事件触发,只会扫描对应容器下有关的listener,我理解这多是这个组件库能够有待改进的地方(或许是个能pr好机会哟~)。
总之,这个函数大体意思也就是在 scroll/resize
事件触发时,集中对涉及到的lazyload组件进行判断他们是否显示加载。java
const lazyLoadHandler = () => {
for (let i = 0; i < listeners.length; ++i) {
const listener = listeners[i];
// 这个函数在ComponentDidMount阶段也被调用过,细节3将会更详细的讲解他的逻辑
checkVisible(listener);
}
// Remove `once` component in listeners
purgePending(); // 这个地方属于非主线细节,就暂时略过了,感兴趣的能够看源码
};
复制代码
const checkVisible = function checkVisible(component) {
const node = ReactDom.findDOMNode(component); // 获取真实的dom元素
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) {
if (component.props.once) { // 这个once的api应该是用来作性能优化的"剪枝"操做的,
pending.push(component); // 避免没必要要的listen再次被扫处处理
} // 这里能够想一想若是是咱们设计这个组件时,是否会考虑到这个api
component.visible = true; // 一旦组件是须要显示的,就会调用 component.forceUpdate
component.forceUpdate(); // 来对组件进行更新操做了
}
} else if (!(component.props.once && component.visible)) { // 这里应该是考虑到被懒加载的组件
component.visible = false; // 后续可能会由于外部props致使
if (component.props.unmountIfInvisible) { // 更新,把非视区的组件先暂时隐藏,
component.forceUpdate(); // 这样想也是另外一场景下的性能优化
}
}
};
复制代码
从上面代码看,做者考虑到了不一样场景下的一些优化性能的方式,基于此设计了相应的once,unmountIfInVisible 的api,可谓是很全面的了,能够想一想假设是咱们本身来设计时,是否能想到这些api,想到了会怎么来设计?node
这是处理正常全屏幕容器懒加载组件是否可见的状况,其实不看代码,咱们也能大概知道,是一个判断当前的组件是否和可视区域有交集的,能够抽象成二维平面,两个四边形是否相交的问题,相交则证实组件属于可视区域,反之亦然。react
const checkNormalVisible = function checkNormalVisible(component) {
const node = ReactDom.findDOMNode(component);
// 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 {
// Element.getBoundingClientRect()方法返回元素的大小及其相对于视口的位置
// 这里只获取了组件的盒模型高及相对的top位置,由此能判断当前的懒加载组件只处理垂直方向的懒加载
({ top, height: elementHeight } = node.getBoundingClientRect());
} catch (e) { // 容错方案,细节可看源码
({ top, height: elementHeight } = defaultBoundingClientRect);
}
// 由于是全屏幕的容器,因此另外一个用来判断是否与组件盒子有交集的四边形就是window了
const windowInnerHeight = window.innerHeight || document.documentElement.clientHeight;
// 这里可的offsets的api设计,能够理解为懒加载的“提早量”须要,作过相似需求的朋友应该能有体会
const offsets = Array.isArray(component.props.offset) ?
component.props.offset :
[component.props.offset, component.props.offset]; // Be compatible with previous API
// 在垂直方向,判断是否有交集的逻辑,为何是这么判断呢
// 其实很好理解,交不交差其实都是按边界状况考虑的,若是组件的上边界相对视窗位置(top-offsets[0])
// 超过了视窗的下边界的位置windowHeight,那不再可能相较了。
// 同理,若是组件的下边界位置,超过了视窗上边界的位置,那一样也不可能再相交,由此得出了这个计算式子
return (top - offsets[0] <= windowInnerHeight) &&
(top + elementHeight + offsets[1] >= 0);
}
复制代码
一样是判断是否相交的逻辑,下面的代码区别于细节4的状况,主要在于容器非全屏幕的状况,容器只是浏览器视窗的一个子集,因此在处理相较逻辑上会稍稍作一些改变,看起来应该要多一些相对距离的计算逻辑,具体咱们来看代码
git
const checkOverflowVisible = function checkOverflowVisible(component, parent) {
const node = ReactDom.findDOMNode(component);
let parentTop;
let parentHeight;
try {
// 因为多了一个非全屏幕的容器,因此此处须要获取父级容器的位置是比较容易理解的
({ top: parentTop, height: parentHeight } = parent.getBoundingClientRect());
} catch (e) {
({ top: parentTop, height: parentHeight } = defaultBoundingClientRect);
}
const windowInnerHeight = window.innerHeight || document.documentElement.clientHeight;
// calculate top and height of the intersection of the element's scrollParent and viewport
// 有了非全屏幕容器的存在,因此须要计算真正可视区域的上边界
const intersectionTop = Math.max(parentTop, 0); // intersection's top relative to viewport
// 获取真正可视区域的高,也就是获取可视区域的下边界
const intersectionHeight = Math.min(windowInnerHeight, parentTop + parentHeight) - intersectionTop; // height
// check whether the element is visible in the intersection
let top;
let height;
try {
({ top, height } = node.getBoundingClientRect());
} catch (e) {
({ top, height } = defaultBoundingClientRect);
}
const offsetTop = top - intersectionTop; // element's top relative to intersection
const offsets = Array.isArray(component.props.offset) ?
component.props.offset :
[component.props.offset, component.props.offset]; // Be compatible with previous API
// 利用求得的实际上下边界,进行与全屏幕视窗时相同的计算方式来进行相交判断,便能计算出组件是否可见了
return (offsetTop - offsets[0] <= intersectionHeight) &&
(offsetTop + height + offsets[1] >= 0);
};
复制代码
因此,其实对于checkOverflow这种状况的相交判断,只是正常相交判断的特殊版本,二者的代码逻辑是一致的,甚至也能够写作一个函数,不过从阅读的感受上来看,这种状况,分开写阅读起来会更友好,更能分清楚不一样的状况,也给咱们日常实现相似逻辑时,提供一点参考。 github
整个仓库的代码不算上测试用例的话,估计不到1千行,但实现了很丰富场景的懒加载的状况,也对不一样场景的性能优化增长了api支持,整个阅读过程下来受到了很多的启发:api