react-infinite-scroller
就是一個組件,主要邏輯就是addEventListener
綁定scroll
事件。node
看它的源碼主要意義不在知道如何使用它,而是知道之後處理滾動加載
要注意的東西。react
此處跳到 總結。
參數:ajax
// 渲染出來的DOM元素name element: 'div', // 是否能繼續滾動渲染 hasMore: false, // 是否在訂閱事件的時候執行事件 initialLoad: true, // 表示當前翻頁的值(每渲染一次遞增) pageStart: 0, // 傳遞ref,返回此組件渲染的 DOM ref: null, // 觸發渲染的距離 threshold: 250, // 是否在window上綁定和處理距離 useWindow: true, // 是否反向滾動,即到頂端後渲染 isReverse: false, // 是否使用捕獲模式 useCapture: false, // 渲染前的loading組件 loader: null, // 自定義滾動組件的父元素 getScrollParent: null,
componentDidMount() { this.pageLoaded = this.props.pageStart; this.attachScrollListener(); }
執行attachScrollListener
chrome
attachScrollListener() { const parentElement = this.getParentElement(this.scrollComponent); if (!this.props.hasMore || !parentElement) { return; } let scrollEl = window; if (this.props.useWindow === false) { scrollEl = parentElement; } scrollEl.addEventListener( 'mousewheel', this.mousewheelListener, this.props.useCapture, ); scrollEl.addEventListener( 'scroll', this.scrollListener, this.props.useCapture, ); scrollEl.addEventListener( 'resize', this.scrollListener, this.props.useCapture, ); if (this.props.initialLoad) { this.scrollListener(); } }
此處經過getParentElement
獲取父組件(用戶自定義父組件或者當前dom的parentNode)瀏覽器
而後綁定了3個事件,分別是scroll
,resize
,mousewheel
dom
前2種都綁定scrollListener
,mousewheel
是一個非標準事件,是不建議在生產模式中使用的。函數
那麼這裏爲何要使用呢?學習
此處的mousewheel
事件是爲了處理chrome
瀏覽器的一個特性(不知道是不是一種bug)。this
stackoverflow:Chrome的滾動等待問題code
上面這個問題主要描述,當在使用滾輪加載,並且加載會觸發ajax請求
的時候,當滾輪到達底部,會出現一個漫長並且無任何動做的等待(長達2-3s)。
window.addEventListener("mousewheel", (e) => { if (e.deltaY === 1) { e.preventDefault() } })
以上綁定能夠消除這個"bug"。
我的並無遇到過這種狀況,不知道是否有遇到過能夠說說解決方案。
getParentElement(el) { const scrollParent = this.props.getScrollParent && this.props.getScrollParent(); if (scrollParent != null) { return scrollParent; } return el && el.parentNode; }
上面用到了getParentElement
,很好理解,使用用戶自定義的父組件,或者當前組件DOM.parentNode
。
scrollListener() { const el = this.scrollComponent; const scrollEl = window; const parentNode = this.getParentElement(el); let offset; // 使用window的狀況 if (this.props.useWindow) { const doc = document.documentElement || document.body.parentNode || document.body; const scrollTop = scrollEl.pageYOffset !== undefined ? scrollEl.pageYOffset : doc.scrollTop; // isReverse指 滾動到頂端,load新組件 if (this.props.isReverse) { // 相反模式獲取到頂端距離 offset = scrollTop; } else { // 正常模式則獲取到底端距離 offset = this.calculateOffset(el, scrollTop); } // 不使用window的狀況 } else if (this.props.isReverse) { // 相反模式組件到頂端的距離 offset = parentNode.scrollTop; } else { // 正常模式組件到底端的距離 offset = el.scrollHeight - parentNode.scrollTop - parentNode.clientHeight; } // 此處應該要判斷確保滾動組件正常顯示 if ( offset < Number(this.props.threshold) && (el && el.offsetParent !== null) ) { // 卸載事件 this.detachScrollListener(); // 卸載事件後再執行 loadMore if (typeof this.props.loadMore === 'function') { this.props.loadMore((this.pageLoaded += 1)); } } }
組件核心。
offsetParent
offsetParent
返回一個指向最近的包含該元素的定位元素.
offsetParent
頗有用,由於計算offsetTop
和offsetLeft
都是相對於offsetParent
邊界的。
ele.offsetParent
爲 null 的3種狀況:
body
position
爲fixed
ele 的display
爲none
此組件中offsetParent
處理了2種狀況
在useWindow
的狀況下(即事件綁定在window,滾動做用在body)
經過遞歸獲取offsetParent
到達頂端的高度(offsetTop
)。
calculateTopPosition(el) { if (!el) { return 0; } return el.offsetTop + this.calculateTopPosition(el.offsetParent); }
經過判斷offsetParent
不爲null的狀況,確保滾動組件正常顯示
if ( offset < Number(this.props.threshold) && (el && el.offsetParent !== null) ) {/* ... */ }
scrollHeight
和clientHeight
在無滾動的狀況下,scrollHeight
和clientHeight
相等,都爲height
+padding
*2
在有滾動的狀況下,scrollHeight
表示實際內容高度,clientHeight
表示視口高度。
loadMore
前卸載事件。確保不會重複(過多)執行loadMore
,由於先卸載事件再執行loadMore
,能夠確保在執行過程當中,scroll
事件是無效的,而後再每次componentDidUpdate
的時候從新綁定事件。
render() { // 獲取porps const renderProps = this.filterProps(this.props); const { children, element, hasMore, initialLoad, isReverse, loader, loadMore, pageStart, ref, threshold, useCapture, useWindow, getScrollParent, ...props } = renderProps; // 定義一個ref // 能將當前組件的DOM傳出去 props.ref = node => { this.scrollComponent = node; // 執行父組件傳來的ref(若是有) if (ref) { ref(node); } }; const childrenArray = [children]; // 執行loader if (hasMore) { if (loader) { isReverse ? childrenArray.unshift(loader) : childrenArray.push(loader); } else if (this.defaultLoader) { isReverse ? childrenArray.unshift(this.defaultLoader) : childrenArray.push(this.defaultLoader); } } // ref 傳遞給 'div'元素 return React.createElement(element, props, childrenArray); }
這裏一個小亮點就是,在react
中,this.props
是不容許修改的。
這裏使用瞭解構
getScrollParent, ...props } = renderProps;
這裏解構至關於Object.assign
,定義了一個新的object
,即可以添加屬性了,而且this.props
不會受到影響。
react-infinite-scroller
邏輯比較簡單。
一些注意/學習/複習點:
Chrome
的一個滾動加載請求的bug。本文位置 offsetParent
的一些實際用法。本文位置 scrollHeight
和clientHeight
區別。本文位置 此庫建議使用在自定義的一些組件上而且不那麼複雜的邏輯上。
用在第三方庫能夠會沒法獲取正確的父組件,而經過document.getElementBy..
傳入。
面對稍微複雜的邏輯,
例如,一個搜索組件,訂閱onChange
事件而且呈現內容,搜索"a",對呈現內容滾動加載了3次,再添加搜索詞"b",這時候"ab"的內容呈現是在3次以後。