本篇文章主要介紹一個優秀的基於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