近来没什么特别要作的事,下班回来的空闲时间也比较多,因此抽空看看懒加载是怎么实现的,特别是看了下 react-lazy-load
的库的实现。css
这里懒加载场景不是路由分割打包那种,而是单个页面中有一个很长的列表,列表中的图片进行懒加载的效果。html
在 jquery
时代,这种列表图片懒加载效果就已经有了,那么咱们想想这种在滚动的时候才去加载图片等资源的方式该如何去实现呢?vue
浏览器解析 html
的时候,在遇到 img
标签以及发现 src
属性的时候,浏览器就会去发请求拿图片去了。这里就是切入点,根据这种现象,作下面几件事:node
img
标签的 src
设为空dom
属性,打个比方: <img data-src='/xxxxx.jpg' />
img
标签上存着真实图片路径赋值给 src
属性知道懒加载的大概原理,来看一下 react-lazy-load
是怎么作的。react
大致看了下 react-lazy-load
的实现的整体思路就更加简单了,本质上就是让须要懒加载的组件包含在这个包提供的 LazyLoad
组件中,不渲染这个组件,而后去监听这个 LazyLoad
组件是否已是可见了,若是是可见了那么就去强制渲染包含在 LazyLoad
组件内部须要懒加载的组件了。jquery
这种方式相较于手动去控制 img
标签来的实在是太方便了,彻底以组件为单位,对组件进行懒加载。这样的话,彻底就不须要感知组件内部的逻辑和渲染逻辑,不管这个须要懒加载的组件内部是有几个 img
标签,也彻底不用去手动操控 src
属性的赋值。数组
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 : <div style={{ height: this.props.height }} className="lazyload-placeholder" /> } }
从 render
函数可以看出来,依据当前 visible
的值来肯定是否渲染 this.props.children
,若是为 false
则去渲染节点的占位符。若是外部传入一个占位节点,就用这个传入的占位节点,不然就用默认的占位符去占位。注意到:shouldComponentUpdate
依据 this.visible
的值去判断是否更新组件。剩下的,该去看看如何监听事件以及修改 this.visible
、强制从新渲染组件的。浏览器
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
检查是否可见。dom
/** * 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
来从新渲染。ide
/** * 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
的时候,组件快要出如今可视范围的时候就会去从新渲染组件而不是出如今可视范围内才去从新渲染。
lazyLoadHandler
是组件绑定事件时会触发的函数。
const lazyLoadHandler = () => { for (let i = 0; i < listeners.length; ++i) { const listener = listeners[i]; checkVisible(listener); } // Remove `once` component in listeners purgePending(); };
每次监听事件执行的时候,都去检查一下组件,若是知足条件就去强制渲染组件。
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
代码很长并且好像比较复杂,因此也就没看了。