react-lazy-load粗读
近来没什么特别要做的事,下班回来的空闲时间也比较多,所以抽空看看懒加载是怎么实现的,特别是看了下 react-lazy-load
的库的实现。
懒加载
这里懒加载场景不是路由分割打包那种,而是单个页面中有一个很长的列表,列表中的图片进行懒加载的效果。
在 jquery
时代,这种列表图片懒加载效果就已经有了,那么我们想一想这种在滚动的时候才去加载图片等资源的方式该如何去实现呢?
大致原理
浏览器解析 html
的时候,在遇到 img
标签以及发现 src
属性的时候,浏览器就会去发请求拿图片去了。这里就是切入点,根据这种现象,做下面几件事:
- 把列表中所有的图片的
img
标签的src
设为空 - 把真实的图片路径存成一个
dom
属性,打个比方: - 写一个检测列表某一项是否是可见状态
- 全局滚动事件做一个监听,检测当前列表的项是否是可见的,如果可见则给
img
标签上存着真实图片路径赋值给src
属性
react-lazy-load
知道懒加载的大概原理,来看一下 react-lazy-load
是怎么做的。
大体看了下 react-lazy-load
的实现的总体思路就更加简单了,本质上就是让需要懒加载的组件包含在这个包提供的 LazyLoad
组件中,不渲染这个组件,然后去监听这个 LazyLoad
组件是否已经是可见了,如果是可见了那么就去强制渲染包含在 LazyLoad
组件内部需要懒加载的组件了。
这种方式相较于手动去控制 img
标签来的实在是太方便了,完全以组件为单位,对组件进行懒加载。这样的话,完全就不需要感知组件内部的逻辑和渲染逻辑,无论这个需要懒加载的组件内部是有几个 img
标签,也完全不用去手动操控 src
属性的赋值。
react-lazy-load 之 render
class LazyLoad extends React.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
:
}
}
从 render
函数能够看出来,依据当前 visible
的值来确定是否渲染 this.props.children
,如果为 false
则去渲染节点的占位符。如果外部传入一个占位节点,就用这个传入的占位节点,否则就用默认的占位符去占位。注意到:shouldComponentUpdate
依据 this.visible
的值去判断是否更新组件。剩下的,该去看看如何监听事件以及修改 this.visible
、强制重新渲染组件的。
react-lazy-load 之 componentDidMount
componentDidMount() {
// It's unlikely to change delay type on the fly, this is mainly
// designed for tests
const needResetFinalLazyLoadHandler = (this.props.debounce !== undefined && delayType === 'throttle')
|| (delayType === 'debounce' && this.props.debounce === undefined);
if (needResetFinalLazyLoadHandler) {
off(window, '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(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) {
const { scroll, resize } = this.props;
if (scroll) {
on(window, 'scroll', finalLazyLoadHandler, passiveEvent);
}
if (resize) {
on(window, 'resize', finalLazyLoadHandler, passiveEvent);
}
}
listeners.push(this);
checkVisible(this);
}
needResetFinalLazyLoadHandler
先别关注,按他给注释说测试用。 finalLazyLoadHandler
依据外部 debounce
和 throttle
来选择是防抖还是节流还是都不用。根据外部传入的overflow
来确定是否是在某一个节点中 overflow
的下拉框的懒加载还是普通的整个 window
的懒加载。然后就是依据是 scroll
还是 resize
来给 window
增加监听事件 finalLazyLoadHandler
。 最后就是把这个组件实例放到了 listeners
这个数组里,然后调用 checkVisible
检查是否可见。
react-lazy-load 之 checkVisible
/**
* Detect if element is visible in viewport, if so, set `visible` state to true.
* If `once` prop is provided true, remove component as listener after checkVisible
*
* @param {React} component React component that respond to scroll and resize
*/
const checkVisible = function checkVisible(component) {
const node = ReactDom.findDOMNode(component);
if (!node) {
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) {
pending.push(component);
}
component.visible = true;
component.forceUpdate();
}
} else if (!(component.props.once && component.visible)) {
component.visible = false;
if (component.props.unmountIfInvisible) {
component.forceUpdate();
}
}
};
parent
就是找到这个组件的上层组件的 dom
节点,通过 checkOverflowVisible
和 checkNormalVisible
这两个函数拿到该节点是否在可视区域内得到 visible
。然后依据 visible
的值修改 component
的 visible
的值,然后调用组件的 forceUpdate
方法,强制让组件重新渲染。主要到组件的 visible
并不是挂载到 state
上,所以这里不是用 setState
来重新渲染。
react-lazy-load 之 checkNormalVisible
/**
* Check if `component` is visible in document
* @param {node} component React component
* @return {bool}
*/
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 {
({ 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);
};
主要逻辑就是拿到组件的 dom
节点的 getBoundingClientRect
返回值和 window.innerHeight
进行比较来判断是否是在可视范围内。这里在比较的时候还有个 component.props.offset
也参与了比较,说明设置了 offset
的时候,组件快要出现在可视范围的时候就会去重新渲染组件而不是出现在可视范围内才去重新渲染。
react-lazy-load 之 lazyLoadHandler
lazyLoadHandler
是组件绑定事件时会触发的函数。
const lazyLoadHandler = () => {
for (let i = 0; i < listeners.length; ++i) {
const listener = listeners[i];
checkVisible(listener);
}
// Remove `once` component in listeners
purgePending();
};
每次监听事件执行的时候,都去检查一下组件,如果满足条件就去强制渲染组件。
react-lazy-load 之 componentWillUnmount
componentWillUnmount() {
if (this.props.overflow) {
const parent = scrollParent(ReactDom.findDOMNode(this));
if (parent && typeof parent.getAttribute === 'function') {
const listenerCount = (+parent.getAttribute(LISTEN_FLAG)) - 1;
if (listenerCount === 0) {
parent.removeEventListener('scroll', finalLazyLoadHandler, passiveEvent);
parent.removeAttribute(LISTEN_FLAG);
} else {
parent.setAttribute(LISTEN_FLAG, listenerCount);
}
}
}
const index = listeners.indexOf(this);
if (index !== -1) {
listeners.splice(index, 1);
}
if (listeners.length === 0) {
off(window, 'resize', finalLazyLoadHandler, passiveEvent);
off(window, 'scroll', finalLazyLoadHandler, passiveEvent);
}
}
组件卸载的时候,把一些绑定事件解绑一下,细节也不说了。
总结
抛开 react-lazy-load
一些实现细节,从总体把握整个懒加载的过程,其实懒加载的原理并不难。当时我也看了一下 vue
那边的 vue-lazyLoad
这个库想写一个对比的文章,我以为这个 vue
库的内容会写的和 react-lazy-load
差不多,结果发现 vue-lazyLoad
代码很长而且好像比较复杂,所以也就没看了。