文章源碼: https://github.com/Haixiang6123/my-react-infinite-scroller
預覽鏈接: http://yanhaixiang.com/my-react-infinite-scroller/
參考輪子: https://www.npmjs.com/package/react-infinite-scroller
無限滾動是一個開發時經常遇到的問題,比如 ant-design 的 List 組件裏就推薦使用 react-infinite-scroller 配合 List 組件一起使用。
假如我們想自己實現無限滾動,難免要去查 scroll
事件,還要搞清 offsetHeight
, scrollHeight
, pageX
這些奇奇怪怪變量之間的關係,真讓人腦袋大。今天就帶大家造一個 reac-infinite-scroller 的輪子吧。
offset 公式
無限滾動的原理很簡單:只要 很長元素總高度 - 窗口距離頂部高度 - 窗口高度 < 閾值
就加載更多,前面那一堆下稱爲 offset
,表示還剩多少 px 到達底部。
然後就懵逼了:scrollY
, pageY
, scrollTop
, offsetTop
, clientHeight
這一堆的變量到底用哪個來計算呢?對於大部分人來說,這些變量簡直是噩夢一般的存在,總是會傻傻搞不清。
這裏直接給出計算 offset 的公式,免得大家去查了:
const offset = 很長元素總高度 - 窗口距離頂部高度 - 窗口高度 = node.scrollHeight - parentNode.scrollTop - parentNode.clientHeight
if (offset < this.props.threshold) {
this.props.loadMore()
}
簡單說一下這些變量都是個啥:
- scrollHeight: 這個只讀屬性是一個元素的內容高度,包括由於溢出導致的視圖中不可見內容。相當於上面的 “很長元素總高度”
- scrollTop: 可以獲取或設置一個元素的內容垂直滾動的像素數。相當於上面的 “窗口距離頂部的高度”
- clientHeight: 僅僅包括 padding 的元素高度。相當於上面的 “窗口高度”
總結一下,上面公式裏的 offset
表示距離底部的 px 值,只要 offset < threshold
說明滾動到了底部,開始 loadMore()
。
最小實現
下面爲使用用例,定義 delay 函數用於 mock 延時效果,fetchMore
爲獲取更多數據的函數。
let counter = 0
const delay = (asyncFn: () => Promise<void>) => new Promise<void>(resolve => {
setTimeout(() => {
asyncFn().then(() => resolve)
}, 1500)
})
const App = () => {
const [items, setItems] = useState<string[]>([]);
const fetchMore = async () => {
await delay(async () => {
const newItems = []
for (let i = counter; i < counter + 50; i++) {
newItems.push(`Counter: ${i} |||| ${new Date().toISOString()}`)
}
setItems([...items, ...newItems])
counter += 50
})
}
useEffect(() => {
fetchMore().then()
}, [])
return (
<div>
<div style={{height: 250, overflow: 'auto', border: '1px solid red'}}>
<InfiniteScroll
throttle={50}
loadMore={fetchMore}
loader={<div className="loader" key={0}>Loading ...</div>}
>
{items.map(item => <div key={item}>{item}</div>)}
</InfiniteScroll>
</div>
</div>
)
}
輪子最簡單的實現如下:
interface Props {
loadMore: Function // 加載更多的回調
loader: ReactNode // “加載更多”的組件
threshold: number // 到達底部的閾值
hasMore?: boolean // 是否還有更多可以加載
pageStart?: number // 頁面初始頁
}
class InfiniteScroll extends Component<Props, any> {
private scrollComponent: HTMLDivElement | null = null // 當前很很長的內容
private loadingMore = false // 是否正在加載更多
private pageLoaded = 0 // 當前加載頁數
constructor(props: Props) {
super(props);
this.scrollListener = this.scrollListener.bind(this) // scrollListener 用到了 this,所以要 bind 一下
}
// 滾動監聽順
scrollListener() {
const node = this.scrollComponent
if (!node) return
const parentNode = node.parentElement
if (!parentNode) return
// 核心計算公式
const offset = node.scrollHeight - parentNode.scrollTop - parentNode.clientHeight
if (offset < this.props.threshold) {
parentNode.removeEventListener('scroll', this.scrollListener) // 加載的時候去掉監聽器
this.props.loadMore(this.pageLoaded += 1) // 加載更多
this.loadingMore = true // 正在加載更多
}
}
componentDidMount() {
this.pageLoaded = this.props.pageStart || 0
// Mount 的時候就添加監聽器
if (this.scrollComponent && this.scrollComponent.parentElement) {
this.scrollComponent.parentElement.addEventListener('scroll', this.scrollListener)
}
}
componentDidUpdate() {
// 到達底部時會把監聽器臨時移除,組件更新的時候,這裏再加回來
if (this.scrollComponent && this.scrollComponent.parentElement) {
this.scrollComponent.parentElement.addEventListener('scroll', this.scrollListener)
}
}
componentWilUnmount() {
// Mount 的時候就添加監聽器
if (this.scrollComponent && this.scrollComponent.parentElement) {
this.scrollComponent.parentElement.addEventListener('scroll', this.scrollListener)
}
}
render() {
const {children, loader} = this.props
// 獲取滾動元素的核心代碼
return (
<div ref={node => this.scrollComponent = node}>
{children} 很長很長很長的東西
{loader} “加載更多”
</div>
)
}
}
上面就是一個最小實現,有以下注意點:
- scrollListener 用到了 this,所以要
bind
this,不然 this 爲undefined
- parentElement 上添加/移除監聽器
- 組件 mount 的時候添加監聽器,
offset < threshold
的時候移除監聽器,組件更新後再次添加監聽器,unmount 前移除監聽器
上面添加/移除監聽器的代碼有點冗餘,封裝一下:
class InfiniteScroll extends Component<Props, any> {
...
// 滾動監聽順
scrollListener() {
const node = this.scrollComponent
if (!node) return
const parentNode = node.parentElement
if (!parentNode) return
// 核心計算公式
const offset = node.scrollHeight - parentNode.scrollTop - parentNode.clientHeight
if (offset < this.props.threshold) {
this.detachScrollListener() // 加載的時候去掉監聽器
this.props.loadMore(this.pageLoaded += 1) // 加載更多
this.loadingMore = true // 正在加載更多
}
}
getParentElement(el: HTMLElement | null): HTMLElement | null {
return el && el.parentElement
}
attachScrollListener() {
const parentElement = this.getParentElement(this.scrollComponent)
if (!parentElement) return
const scrollEl = this.props.useWindow ? window : parentElement
scrollEl.addEventListener('scroll', this.scrollListener)
scrollEl.addEventListener('resize', this.scrollListener)
}
detachScrollListener() {
const parentElement = this.getParentElement(this.scrollComponent)
if (!parentElement) return
parentElement.removeEventListener('scroll', this.scrollListener)
parentElement.removeEventListener('resize', this.scrollListener)
}
componentDidMount() {
this.attachScrollListener()
}
componentDidUpdate() {
this.attachScrollListener()
}
componentWillUnmount() {
this.detachScrollListener()
}
render() {
const {children, loader} = this.props
// 獲取滾動元素的核心代碼
return (
<div ref={node => this.scrollComponent = node}>
{children} 很長很長很長的東西
{loader} “加載更多”
</div>
)
}
}
上面首先將獲取 parentElement
的動作抽象出來,再把 attachScrollListener
和 detachScrollListener
抽象出來。同時,上面還對 resize 事件綁定了監聽器,因爲當用戶 resize 的時候也會出現 offset < threshold
的可能,這個時候也需要 loadMore
。
還有一個問題:剛進頁面的時候,高度爲 0,假如此時 offset < threshold
理應觸發“加載更多”,然而這個時候用戶並沒有做任何滾動,滾動事件不會被觸發,“加載更多”也不會被觸發,這其實並不符合我們的預期。
因此,這裏可以加一個 initialLoad
的 props 指定添加監聽器的時候就自動觸發一次監聽器的代碼。
interface Props {
...
initialLoad?: boolean // 是否第一次就加載
}
class InfiniteScroll extends Component<Props, any> {
...
attachListeners() {
const parentElement = this.getParentElement(this.scrollComponent)
if (!parentElement) return
parentElement .addEventListener('scroll', this.scrollListener, this.eventOptions)
parentElement .addEventListener('resize', this.scrollListener, this.eventOptions)
if (this.props.initialLoad) {
this.scrollListener()
}
}
}
useWindow
上面對 parentElement
的限制是比較死的,可以添加 getParentElement
這個 props 讓開發者自己指定 parentElement
,這樣輪子就會更靈活些。
interface Props {
loadMore: Function
loader: ReactNode
threshold: number
getScrollParent?: () => HTMLElement
}
class InfiniteScroll extends Component<Props, any> {
...
getParentElement(el: HTMLElement | null): HTMLElement | null {
const scrollParent = this.props.getScrollParent && this.props.getScrollParent()
if (scrollParent) {
return scrollParent
}
return el && el.parentElement
}
...
}
此時們不禁想到,要是開發者想傳 document.body
作爲 parentElement
,上面的代碼還能繼續使用麼?當然是不行的。document.body
和很長很長的元素往往存在很多層嵌套,這些複雜的嵌套關係有時候並不會是我們希望的那樣。
而在全局 (window) 做無限滾動的例子又比較常見,爲了實現全局滾動的功能,這裏加一個 useWindow
props 來表示是否用 window
作爲滾動的容器。
interface Props {
...
getScrollParent?: () => HTMLElement // 獲取 parentElement 的回調
useWindow?: boolean // 是否以 window 作爲 scrollEl
}
如果用全局作爲滾動容器,我們需要另一套算計方法來算 offset
了,下面給出新的計算公式:
offset = 很長元素總高度 - 窗口距離頂部高度 - 窗口高度 = (當前窗口頂部與很長元素頂部的距離 + offsetHeight) - window.pageYOffset - window.innerHeight
- offsetHeight: 是一個只讀屬性,返回一個元素的佈局高度
- window.pageYOffset: 其實就是 scrollY 的別名,返回文檔在垂直方向已滾動的像素值
- window.innerHeight: 爲瀏覽器窗口的視口的高度
上面公式裏“當前窗口頂部與很長元素頂部的距離 + offsetHeigh”在頁面裏是定死的,而 window.pageYOffset - window.innerHeight
會隨着滾動而改變,兩者相減則爲 offset
。圖示:
不過,這裏的 “當前窗口頂部與很長元素頂部的距離” 這一步並不能通過變量來獲得,只能用 JS 來獲取:
// 元素頂部到頁面頂部的距離
calculateTopPosition(el: HTMLElement | null): number {
if (!el) return 0
return el.offsetTop + this.calculateTopPosition(el.offsetParent as HTMLElement)
}
利用 calculateTopPosition
函數,計算 offset
的函數爲:
// 計算 offset
calculateOffset(el: HTMLElement | null, scrollTop: number) {
if (!el) return 0
return this.calculateTopPosition(el) + (el.offsetHeight - scrollTop - window.innerHeight)
}
整理上面函數到輪子裏:
class InfiniteScroll extends Component<Props, any> {
...
scrollListener() {
const node = this.scrollComponent
if (!node) return
const parentNode = this.getParentElement(node)
if (!parentNode) return
let offset;
if (this.props.useWindow) {
const doc = document.documentElement || document.body.parentElement || document.body // 全局滾動容器
const scrollTop = window.pageYOffset || doc.scrollTop // 全局的 "scrollTop"
offset = this.calculateOffset(node, scrollTop)
} else {
offset = node.scrollHeight - parentNode.scrollTop - parentNode.clientHeight
}
if (offset < this.props.throttle) {
node.removeEventListener('scroll', this.scrollListener)
this.props.loadMore(this.pageLoaded += 1)
this.loadingMore = true
}
}
calculateOffset(el: HTMLElement | null, scrollTop: number) {
if (!el) return 0
return this.calculateTopPosition(el) + el.offsetHeight - scrollTop - window.innerHeight
}
calculateTopPosition(el: HTMLElement | null): number {
if (!el) return 0
return el.offsetTop + this.calculateTopPosition(el.offsetParent as HTMLElement)
}
attachScrollListener() {
const parentElement = this.getParentElement(this.scrollComponent)
if (!parentElement) return
const scrollEl = this.props.useWindow ? window : parentElement
scrollEl.addEventListener('scroll', this.scrollListener)
}
detachScrollListener() {
const parentElement = this.getParentElement(this.scrollComponent)
if (!parentElement) return
const scrollEl = this.props.useWindow ? window : parentElement
scrollEl .removeEventListener('scroll', this.scrollListener)
}
...
}
上面改動的點有:
- 添加和移除監聽器時,如果
useWindow === true
,以window
爲scrollEl
- 添加計算 topPosition 和 offset 的函數:
calculateTopPosition
和calculateOffset
- 監聽器裏判斷是否
useWindow
,如果true
,使用上面的calculateOffset
計算 offset
至此,無限滾動最核心的滾動已經實現了。
isReverse
除了向下無限滾動,我們還要考慮無限向上滾動的情況。有人就會問了:一般都是無限向下的呀,哪來的無限向上?很簡單,翻找微信的聊天記錄不就是無限向上滾動的嘛。
首先,在 props 加一個 isReverse
用於指定向下還是向上無限滾動。
interface Props {
...
isReverse?: boolean // 是否爲相反的無限滾動
}
那 isReverse
會影響哪個部分呢?第一反應肯定是 loader
的位置變了:
render() {
const {children, loader, isReverse} = this.props
const childrenArray = [children]
if (loader) {
// 根據 isReverse 改變 loader 的插入方式
isReverse ? childrenArray.unshift(loader) : childrenArray.push(loader)
}
return (
<div ref={node => this.scrollComponent = node}>
{childrenArray}
</div>
)
}
然後 offset
的計算也要變了。對於向上無限滾動,offset
的計算反而變簡單了,直接 offset = scrollTop
。在 scrollListener
裏修改 offset
的計算:
scrollListener() {
const el = this.scrollComponent
if (!el) return
const parentElement = this.getParentElement(el)
if (!parentElement) return
let offset;
if (this.props.useWindow) {
const doc = document.documentElement || document.body.parentElement || document.body
const scrollTop = window.pageYOffset || doc.scrollTop
offset = this.props.isReverse ? scrollTop : this.calculateOffset(el, scrollTop)
} else {
offset = this.props.isReverse
? parentElement.scrollTop
: el.scrollHeight - parentElement.scrollTop - parentElement.clientHeight
}
// 是否到達閾值,是否可見
if (offset < (this.props.threshold || 300) && (el && el.offsetParent !== null)) {
this.detachListeners()
this.beforeScrollHeight = parentElement.scrollHeight
this.beforeScrollTop = parentElement.scrollTop
if (this.props.loadMore) {
this.props.loadMore(this.pageLoaded += 1)
this.loadingMore = true
}
}
}
我們還要考慮一個問題:向上滾動加載更多內容後,滾動條的位置不應該還停留在 scrollY = 0 的位置,不然會一直加載更多,比如此時滾動到了頂部:
3 <- 到頂部了,開始加載
2
1
0
加載更多後
6 <- 不應該停留在這個位置,因爲會再次觸發無限滾動,用戶體驗不友好
5
4
3 <- 應該停留在原始的位置,用戶再向上滾動纔再次加載更多
2
1
0
爲了達到這個效果,我們要記錄上一次的 scrollTop
和 scrollHeight
,然後在組件更新的時候更新 parentElemnt.scrollTop
:
// 當前 scrollTop = 當前 scrollHeight - 上一次的 scrollHeight + 上一交的 scrollTop
parentElement.scrollTop = parentElement.scrollHeight - this.beforeScrollHeight + this.beforeScrollTop
以上面的例子舉例:
- parentElement.scrollHeight: 6 - 0 的高度
- beforeScrollHeight: 3 - 0 的高度
- beforeScrollTop: 高度爲 0
最後更新 parentElement.scrollTop
爲 3 - 0 的高度,滾動條會停留在 3 這個位置。
實現時,首先聲明 beforeScrollHeight
和 beforeScrollTop
,並在 scrollListener
裏進行賦值:
class InfiniteScroll extends Component<Props, any> {
...
// isReverse 後專用參數
private beforeScrollTop = 0 // 上次滾動時 parentNode 的 scrollTop
private beforeScrollHeight = 0 // 上次滾動時 parentNode 的 scrollHeight
...
scrollListener() {
const el = this.scrollComponent
if (!el) return
const parentNode = this.getParentElement(el)
if (!parentNode) return
let offset;
if (this.props.useWindow) {
const doc = document.documentElement || document.body.parentNode || document.body
const scrollTop = window.pageYOffset || doc.scrollTop
offset = this.props.isReverse ? scrollTop : this.calculateOffset(el, scrollTop)
} else if (this.props.isReverse) {
offset = parentNode.scrollTop
} else {
offset = el.scrollHeight - parentNode.scrollTop - parentNode.clientHeight
}
if (offset < (this.props.throttle || 300)) {
this.detachScrollListener()
this.beforeScrollTop = parentNode.scrollTop // 記錄上一次的 scrollTop
this.beforeScrollHeight = parentNode.scrollHeight // 記錄上一次的 scrollHeight
if (this.props.loadMore) {
this.props.loadMore(this.pageLoaded += 1)
this.loadingMore = true
}
}
}
...
}
然後在 componentDidUpdate
裏計算並更新滾動條的位置:
componentDidUpdate() {
if (this.props.isReverse && this.props.loadMore) {
const parentElement = this.getParentElement(this.scrollComponent)
if (parentElement) {
// 更新滾動條的位置
parentElement.scrollTop = parentElement.scrollHeight - this.beforeScrollHeight + this.beforeScrollTop
this.loadingMore = false
}
}
this.attachScrollListener()
}
至此,向上滾動也被我們實現了。
mousewheel 事件
在 Stackoverflow 這個帖子 中說到:Chrome 下做無限滾動時可能存在加載時間變得超長的問題。
目前猜測因爲 passive listener 的特性所引發的,帖子裏也給出瞭解決方法:在 mousewheel 裏 e.preventDefault
就好。
class InfiniteScroll extends Component<Props, any> {
...
mousewheelListener(e: Event) {
// 詳見: https://stackoverflow.com/questions/47524205/random-high-content-download-time-in-chrome/47684257#47684257
// @ts-ignore mousewheel 事件裏存在 deltaY
if (e.deltaY === 1) {
e.preventDefault()
}
}
attachListeners() {
const parentElement = this.getParentElement(this.scrollComponent)
if (!parentElement || !this.props.hasMore) return
const scrollEl = this.props.useWindow ? window : parentElement
scrollEl.addEventListener('scroll', this.scrollListener)
scrollEl.addEventListener('resize', this.scrollListener)
scrollEl.addEventListener('mousewheel', this.mousewheelListener)
}
detachMousewheelListener() {
const scrollEl = this.props.useWindow ? window : this.scrollComponent?.parentElement
if (!scrollEl) return
scrollEl.removeEventListener('mousewheel', this.mousewheelListener)
}
detachListeners() {
const scrollEl = this.props.useWindow ? window : this.getParentElement(this.scrollComponent)
if (!scrollEl) return
scrollEl.removeEventListener('scroll', this.scrollListener)
scrollEl.removeEventListener('resize', this.scrollListener)
}
componentWillUnmount() {
this.detachListeners()
this.detachMousewheelListener()
}
render() {
...
}
}
上面同時把 attachScrollListener
改爲 attachListeners
,並在裏面添加 mousewheel 的監聽器,在 componentWillUnmount
裏移除 mousewheel 的監聽器。
passive listener
上面提到了 passive listener,當監聽器添加了 passive 屬性 後,它就是 passive listener(被動監聽器)。對 touch 和 mouse 的事件監聽不會阻塞頁面的滾動,可提高頁面滾動性能。詳情可見這篇文章。
這裏的兩個監聽器都可以設置 passive: true 來提高滾動性能,不過我們第一步是要檢測當前瀏覽器是否支持被動監聽器。
isPassiveSupported() {
let passive = false
const testOptions = {
get passive() {
passive = true
return true
}
}
try {
const testListener = () => {
}
document.addEventListener('test', testListener, testOptions)
// @ts-ignore 僅用於測試是否可以使用 passive listener
document.removeEventListener('test', testListener, testOptions)
} catch (e) {
}
return passive
}
上面給一個“假的”事件添加了一個“假的”被動監聽器,並帶個 testOptions
作爲第三個參數。testOptions
利用 ES6 Proxy 的特性判斷當前瀏覽器是否會讀取 passive
屬性,讀取了說明支持 passive listener,返回 true
。
再造一個函數獲取監聽器的 options
,這個 options
包含了 passive
和 useCapture
,前者爲是否開啓 passive 特性,後者爲是否捕獲。
interface EventListenerOptions {
useCapture: boolean // 是否捕獲
passive: boolean // 是否 passive
}
class InfiniteScroll extends Component<Props, any> {
...
private eventOptions = {} // 註冊事件的選項
...
isPassiveSupported() { // 當前是否支持 passive
...
}
mousewheelListener(e: Event) {
// 詳見: https://stackoverflow.com/questions/47524205/random-high-content-download-time-in-chrome/47684257#47684257
// @ts-ignore mousewheel 事件裏存在 deltaY
if (e.deltaY === 1 && !this.isPassiveSupported()) {
e.preventDefault()
}
}
getEventListenerOptions() { // 獲取監聽器的 options
const options: EventListenerOptions = {useCapture: this.props.useCapture || false, passive: false}
if (this.isPassiveSupported()) {
options.passive = true
}
return options
}
attachListeners() {
const parentElement = this.getParentElement(this.scrollComponent)
if (!parentElement || !this.props.hasMore) return
const scrollEl = this.props.useWindow ? window : parentElement
scrollEl.addEventListener('mousewheel', this.mousewheelListener, this.eventOptions) // 使用 eventOptions
scrollEl.addEventListener('scroll', this.scrollListener, this.eventOptions) // 使用 eventOptions
scrollEl.addEventListener('resize', this.scrollListener, this.eventOptions) // 使用 eventOptions
if (this.props.initialLoad) {
this.scrollListener()
}
}
detachMousewheelListener() {
const scrollEl = this.props.useWindow ? window : this.scrollComponent?.parentElement
if (!scrollEl) return
scrollEl.removeEventListener('mousewheel', this.mousewheelListener, this.eventOptions) // 使用 eventOptions
}
detachListeners() {
const scrollEl = this.props.useWindow ? window : this.getParentElement(this.scrollComponent) // 使用 eventOptions
if (!scrollEl) return
scrollEl.removeEventListener('scroll', this.scrollListener, this.eventOptions) // 使用 eventOptions
scrollEl.removeEventListener('resize', this.scrollListener, this.eventOptions) // 使用 eventOptions
}
...
}
注意:被動監聽器裏是不能有 e.preventDefault
的,因此在 mousewheelListener
裏要做 isPassiveSupported
的判斷,如果支持了 passive,就不執行 e.preventDefault
。
優化 render 函數
最後,render
函數還可以再進一步優化。首先,在 props 裏添加 element
和 ref
,前者爲容器的 tagName,後者爲獲取滾動元素的回調:
interface Props {
...
element?: string // 元素 tag 名
ref?: (node: HTMLElement | null) => void // 獲取要滾動的元素
}
然後改寫 render
render() {
const {
// 內部 props
children, element, hasMore, isReverse, loader, loadMore, initialLoad,
pageStart, ref, threshold, useCapture, useWindow, getScrollParent,
// 需要 pass 的 props
...props
} = this.props
const childrenArray = [children]
if (hasMore && loader) {
isReverse ? childrenArray.unshift(loader) : childrenArray.push(loader)
}
const passProps = {
...props,
ref: (node: HTMLElement | null) => {
this.scrollComponent = node
if (ref) {
ref(node)
}
}
}
return createElement(element || 'div', passProps, childrenArray)
}
這一步主要優化了 3 個點:
- 將 tagName (
element
props) 也作爲 props 暴露出來 - 將剩下的 props 透傳給滾動元素
- 在
passProps
裏添加ref
,開發者可以通過ref
獲取滾動元素
總結
這篇文章主要帶大家過了一遍 react-infinite-scroller 的源碼,從 0 到 1 地實現了一遍源碼。
核心部分爲 offset < threshold
則加載更多,offset
的計算規則如下:
- 向下滾動:
el.scrollHeight - parentElement.scrollTop - parentElement.clientHeight
- 下上滾動:
parentElement.scrollTop
- window 向下滾動:
calculateTopPosition(el) + (el.offsetHeight - scrollTop - window.innerHeight)
- 其中 calculateTopPosition 爲遞歸地計算元素頂部到瀏覽器窗口頂部的距離
- window 向上滾動:
window.pageYOffset || doc.scrollTop
- 其中 doc 爲
doc = document.documentElement || document.body.parentElement || document.body
當然,這個輪子還有很多細節值得我們注意:
- 除了 scroll 事件,resize 事件也應該觸發加載更多
- 在 mount 和 update 的時候添加 listener,在 unmounte 和
offset < threshold
時移除 listener。還有一點,在添加 listener 的時候可以觸發一次 listener 作爲initialLoad
- 向上滾動的時候,在
componentDidUpdate
裏要把滾動條設置爲上一次停留的地方,否則滾動條會一直在頂部,一直觸發“加載更多” - 在 mousewheel 裏
e.preventDefault
解決“加載更多”時間超長的問題 - 添加被動監聽器,提高頁面滾動性能