近來沒什麼特別要作的事,下班回來的空閒時間也比較多,因此抽空看看懶加載是怎麼實現的,特別是看了下 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
代碼很長並且好像比較複雜,因此也就沒看了。