長列表渲染、無限下拉也算是前端開發老生常談的問題之一了,本文將介紹一種簡潔、巧妙、高效的方式來實現。話很少說,看下圖,也許你能夠發現什麼?前端
不知你是否從上面這張圖中注意到了什麼,好比只是渲染了可視區域的部分 DOM ,滾動過程當中只是外層容器的 padding 在改變?git
前一點很好理解,咱們考慮到性能,不可能將一個長列表(甚至是一個無限下拉列表)的全部列表元素都進行渲染;然後一點,則是本文所介紹方案的核心之一!github
不賣關子,提早告訴你該方案的要素就是兩個:web
說明了要素,也許你能夠嘗試着開始思考,看你是否能猜到具體的實現方案。npm
一直以來,檢測元素的可視狀態或者兩個元素的相對可視狀態都不是件容易事。傳統的各類方案不但複雜,並且性能成本很高,好比須要監聽滾動事件,而後查詢 DOM , 獲取元素高度、位置,計算距離視窗高度等等。數組
這就是 Intersection Observer 要解決的問題。它爲開發人員提供一種便捷的新方法來異步查詢元素相對於其餘元素或視窗的位置,消除了昂貴的 DOM 查詢和樣式讀取成本。緩存
主要在 Safari 上兼容性較差,須要 12.2 及以上才兼容,不過還好,有 polyfill 可食用。dom
基本瞭解 Intersection Observer 以後,接下來就看下如何用 Intersection Observer + padding 來實現無限下拉。異步
先概覽下整體思路:函數
核心:利用父元素的 padding 去填充隨着無限下拉而本該有的、愈來愈多的 DOM 元素,僅僅保留視窗區域上下必定數量的 DOM 元素來進行數據渲染。
// 觀察者建立 this.observer = new IntersectionObserver(callback, options); // 觀察列表第一個以及最後一個元素 this.observer.observe(this.firstItem); this.observer.observe(this.lastItem);
咱們以在頁面中渲染固定的 20 個列表元素爲例,咱們對第一個元素和最後一個元素,用 Intersection Observer 進行觀察,當他們其中一個從新進入視窗時,callback 函數就會觸發:
const callback = (entries) => { entries.forEach((entry) => { if (entry.target.id === firstItemId) { // 當第一個元素進入視窗 } else if (entry.target.id === lastItemId) { // 當最後一個元素進入視窗 } }); };
拿具體例子來講明,咱們用一個數組來維護須要渲染到頁面中的數據。數組的長度會隨着不斷請求新的數據而不斷變大,而渲染的始終是其中必定數量的元素,好比 20 個。
那麼:
// 咱們對原先的 firstIndex 作了緩存 const { currentIndex } = this.domDataCache; // 以所有容器內全部元素的一半做爲每一次渲染的增量 const increment = Math.floor(this.listSize / 2); let firstIndex; if (isScrollDown) { // 向下滾動時序號增長 firstIndex = currentIndex + increment; } else { // 向上滾動時序號減小 firstIndex = currentIndex - increment; }
整體來講,更新 firstIndex,是爲了根據頁面的滾動狀況,知道接下來哪些數據應該被獲取、渲染。
const renderFunction = (firstIndex) => { // offset = firstIndex, limit = 10 => getData // getData Done => new dataItems => render DOM };
這一部分就是根據 firstIndex 查詢數據,而後將目標數據渲染到頁面上便可。
既然數據的更新以及 DOM 元素的更新咱們已經實現了,那麼無限下拉的效果以及滾動的體驗,咱們要如何實現呢?
想象一下,拋開一切,最原始最直接最粗暴的方式無非就是咱們再又獲取了 10 個新的數據元素以後,再塞 10 個新的 DOM 元素到頁面中去來渲染這些數據。
但此時,對比上面這個粗暴的方案,咱們的方案是:這 10個新的數據元素,咱們用原來已有的 DOM 元素去渲染,替換掉已經離開視窗、不可見的數據元素;而本該由更多 DOM 元素進一步撐開容器高度的部分,咱們用 padding 填充來模擬實現。
// padding的增量 = 每個item的高度 x 新的數據項的數目 const remPaddingsVal = itemHeight * (Math.floor(this.listSize / 2)); if (isScrollDown) { // paddingTop新增,填充頂部位置 newCurrentPaddingTop = currentPaddingTop + remPaddingsVal; if (currentPaddingBottom === 0) { newCurrentPaddingBottom = 0; } else { // 若是原來有paddingBottom則減去,會有滾動到底部的元素進行替代 newCurrentPaddingBottom = currentPaddingBottom - remPaddingsVal; } }
// padding的增量 = 每個item的高度 x 新的數據項的數目 const remPaddingsVal = itemHeight * (Math.floor(this.listSize / 2)); if (!isScrollDown) { // paddingBottom新增,填充底部位置 newCurrentPaddingBottom = currentPaddingBottom + remPaddingsVal; if (currentPaddingTop === 0) { newCurrentPaddingTop = 0; } else { // 若是原來有paddingTop則減去,會有滾動到頂部的元素進行替代 newCurrentPaddingTop = currentPaddingTop - remPaddingsVal; } }
// 容器padding從新設置 this.updateContainerPadding({ newCurrentPaddingBottom, newCurrentPaddingTop }) // DOM元素相關數據緩存更新 this.updateDomDataCache({ currentPaddingTop: newCurrentPaddingTop, currentPaddingBottom: newCurrentPaddingBottom });
利用 Intersection Observer 來監測相關元素的滾動位置,異步監聽,儘量得減小 DOM 操做,觸發回調,而後去獲取新的數據來更新頁面元素,而且用調整容器 padding 來替代了本該愈來愈多的 DOM 元素,最終實現列表滾動、無限下拉。
這裏和較爲有名的庫 - iScroll 實現的無限下拉方案進行一個基本的對比,對比以前先說明下 iScroll infinite 的實現概要:
iScroll 經過對傳統滾動事件的監聽,獲取滾動距離,而後:
相關對比:
這是一個同步渲染的方案,也就是目前容器 padding 的計算調整,沒法計算異步獲取的數據,只跟用戶的滾動行爲有關。這看起來與實際業務場景有些不符。解決思路:
本文發佈自 網易雲音樂前端團隊,歡迎自由轉載,轉載請保留出處。咱們一直在招人,若是你剛好準備換工做,又剛好喜歡雲音樂,那就 加入咱們