react-infinite-scroller
就是一個組件,主要邏輯就是addEventListener
綁定scroll
事件。
看它的源碼主要意義不在知道如何使用它,而是知道以後處理滾動加載
要注意的東西。
此處跳到總結。
初識
參數:
// 渲染出來的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
componentDidMount() {
this.pageLoaded = this.props.pageStart;
this.attachScrollListener();
}
執行attachScrollListener
attachScrollListener
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
前2種都綁定scrollListener
,mousewheel
是一個非標準事件,是不建議在生產模式中使用的。
那麼這裏爲什麼要使用呢?
mousewheel解決chrome的等待bug
此處的mousewheel
事件是爲了處理chrome
瀏覽器的一個特性(不知道是否是一種bug)。
上面這個問題主要描述,當在使用滾輪加載,而且加載會觸發ajax請求
的時候,當滾輪到達底部,會出現一個漫長而且無任何動作的等待(長達2-3s)。
window.addEventListener("mousewheel", (e) => {
if (e.deltaY === 1) {
e.preventDefault()
}
})
以上綁定可以消除這個"bug"。
個人並沒有遇到過這種情況,不知道是否有遇到過可以說說解決方案。
getParentElement
getParentElement(el) {
const scrollParent =
this.props.getScrollParent && this.props.getScrollParent();
if (scrollParent != null) {
return scrollParent;
}
return el && el.parentNode;
}
上面用到了getParentElement
,很好理解,使用用戶自定義的父組件,或者當前組件DOM.parentNode
。
scrollListener
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種情況:- ele 爲
body
- ele 的
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) ) {/* ... */ }
-
- ele 爲
-
scrollHeight
和clientHeight
在無滾動的情況下,
scrollHeight
和clientHeight
相等,都爲height
+padding
*2在有滾動的情況下,
scrollHeight
表示實際內容高度,clientHeight
表示視口高度。 - 每次執行
loadMore
前卸載事件。確保不會重複(過多)執行
loadMore
,因爲先卸載事件再執行loadMore
,可以確保在執行過程中,scroll
事件是無效的,然後再每次componentDidUpdate
的時候重新綁定事件。
render
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次之後。