小程序列表懶加載的實現(xiàn)方式
小程序上的列表懶加載
長列表我們經(jīng)常接觸到,長列表為什么需要懶加載呢,因為一旦渲染內(nèi)容多了,渲染引擎就需要更多的時間去渲染畫面,這時可能會出現(xiàn)頁面白屏、卡頓等。而用戶其實只需要看到視窗內(nèi)的內(nèi)容就可以了,不用一次性把全部內(nèi)容渲染完,所以可以通過懶加載實現(xiàn)。
分頁加載
常見的列表懶加載是和后端一起實現(xiàn),也就是分頁加載。前端請求第幾頁的數(shù)據(jù),后端就返回第幾頁的數(shù)據(jù)。前端要實現(xiàn)的交互就是當用戶滑動到頁面底部時,就要請求下一頁的數(shù)據(jù)。
用scroll事件監(jiān)聽
高度示意圖
監(jiān)聽scroll事件,事件回調(diào)會有當前元素的滾動距離scrollTop,當scrollTop+screenHeight等于滾動高度scrollHeight時,表示已經(jīng)滑動到底部。
在小程序中,Page對象提供onReachBottomapi
onReachBottom: function() { // 頁面觸底時執(zhí)行 },
用IntersectionObserver監(jiān)聽
用滾動監(jiān)聽會非常耗性能,滾動時頻繁觸發(fā)回調(diào)的,所以會不斷去計算判斷。比較好的優(yōu)化方案是IntersectionObserverAPI,當IntersectionObserver監(jiān)聽的元素與可視區(qū)有相交狀態(tài)時,就會產(chǎn)生回調(diào),這樣就減少了觸發(fā)的頻率
Page({ onLoad: function(){ wx.createIntersectionObserver().relativeToViewport({bottom: 100}).observe('.target-class', (res) => { res.intersectionRatio // 相交區(qū)域占目標節(jié)點的布局區(qū)域的比例,不等于0時表示有相交 res.intersectionRect // 相交區(qū)域 res.intersectionRect.left // 相交區(qū)域的左邊界坐標 res.intersectionRect.top // 相交區(qū)域的上邊界坐標 res.intersectionRect.width // 相交區(qū)域的寬度 res.intersectionRect.height // 相交區(qū)域的高度 }) } })
前端分頁渲染
上面說的都是前端結合接口的分頁加載。假如說接口沒有分頁,直接就返回了龐大的數(shù)據(jù)列表。前端如果直接就setData所有數(shù)據(jù),會渲染很久,其實復雜的列表渲染20條的時候就已經(jīng)很慢了。這個時候需要對已經(jīng)獲取到的數(shù)據(jù)進行分頁,分批進行渲染。
通過右側(cè)面板可以看到,一開始沒有渲染所有節(jié)點,在滑動頁面過程中節(jié)點再不斷增加。
直接上代碼
<!-- pages/first/index.wxml --> <view class="container"> <block wx:for="{{goodsList}}" wx:key="index" wx:for-item="subItemList"> <block wx:for="{{subItemList}}" wx:key="name"> <view class="item">{{item.name}}</view> </block> </block> </view>
goodsList是一個二維數(shù)組,用wx:for
雙重遍歷
// pages/first/index.js const list = Array.from({length: 5}, (item, index) => { return Array.from({length: Math.ceil(Math.random()*10 + 5)}, (subItem, subIndex) => { return {name: `第${index+1}屏, 第${subIndex+1}個`} }) }) /** 生成的list是一個二維數(shù)組 [ [{}, {}], [{}, {}], [{}, {}], ... ] **/ Page({ data: { goodsList: [], lazyloadIdx: 0 }, onLoad() { this.setData({ goodsList: [list[0]], lazyloadIdx: 1 }) }, // 滑動到底部時往goodsList添加數(shù)據(jù) onReachBottom () { console.log('onReachBottom'); let { lazyloadIdx } = this.data if (lazyloadIdx >= list.length) return this.setData({ [`goodsList[${lazyloadIdx}]`]: list[lazyloadIdx], lazyloadIdx: lazyloadIdx+1 }) } })
每次只setData一屏數(shù)據(jù),既減少了setData數(shù)據(jù)量,又減少渲染時間
用IntersectionObserver代替onReachBottom
有很多場景用不了onReachBottom,那我們只能用IntersectionObserver代替。優(yōu)化一下上面的代碼
# pages/second/index.wxml <view class="container"> <block wx:for="{{goodsList}}" wx:key="index" wx:for-item="subItemList"> <block wx:for="{{subItemList}}" wx:key="name"> <view class="item">{{item.name}}</view> </block> </block> + <view id="observer" class="bottom"></view> </view>
增加節(jié)點用來監(jiān)聽
// pages/second/index.js let lazyloadOb = null Page({ data: { goodsList: [], lazyloadIdx: 0 }, onLoad() { this.setData({ goodsList: [list[0]], lazyloadIdx: 1 }) this.initObserver() }, onunload () { this.disconnenct() }, lazyloadNext () { console.log('lazyloadNext'); let { lazyloadIdx } = this.data if (lazyloadIdx >= list.length) return this.setData({ [`goodsList[${lazyloadIdx}]`]: list[lazyloadIdx], lazyloadIdx: lazyloadIdx+1 }) }, initObserver () { lazyloadOb = wx.createIntersectionObserver().relativeToViewport({ bottom: 50 }).observe('#observer', (res) => { console.log('res.intersectionRatio', res.intersectionRatio); // 觸發(fā)回調(diào)時加載下一屏 if (res.intersectionRatio) this.lazyloadNext() }) }, disconnenct() { lazyloadOb.disconnenct() } })
加需求!
后端返回的商品列表只是一個一維數(shù)組,需要前端轉(zhuǎn)為二維數(shù)組,現(xiàn)在需要每屏的列表長度為5。
假設商品列表個數(shù)為21,那么會生成二維數(shù)組對應的子項長度:
// 偽代碼 [5, 5, 5, 5, 1]
我們可以設定分頁大小pageSize為5,當前分頁pageNum,然后list.slice(pageNum, pageSize)
就能截取對應的數(shù)據(jù),再加入到二維數(shù)組中。
但是產(chǎn)品來加需求了,商品列表默認只展示非售罄商品+一個售罄商品,其余售罄商品要點擊【查看更多】按鈕才展示
假設非售罄商品有16個,售罄11個,每屏的列表長度還是5,那么:
[ 5, 5, 5, // 非售罄 2, // 非售罄+售罄 5, 5 // 售罄 ]
只有兩個的長度不大適合再分一屏,所以小于5時,直接跟前面的合并
[ 5, 5, 7, // 非售罄+一個售罄 5, 5 // 售罄 ]
這個時候設定pageSize就沒法滿足了,所以要根據(jù)售罄個數(shù),非售罄個數(shù)以及一屏長度,算出長度數(shù)組,再算出對應的二維數(shù)組
/** * @desc 生成商品列表的子項長度 * 展示列表包含售罄的,在非售罄列表最后拼接一個售罄商品,點擊展開再加載售罄 * * @param {number} onSaleLen 非售罄長度 * @param {number} soldOutLen 售罄長度 * @returns { { subSize: Array<number>; soldOutLen: number } } */ genSubListSize (onSaleLen, soldOutLen) { // 有售罄的時候,放一項售罄到非售罄那邊去 if (soldOutLen) { soldOutLen-= 1 onSaleLen+=1 } const arr = [] const lazyloadListPartLength = 5 // 一屏5個 let firstSize = 0 if (onSaleLen < lazyloadListPartLength*2) { firstSize = onSaleLen onSaleLen = 0 } else { firstSize = lazyloadListPartLength onSaleLen -= lazyloadListPartLength } arr.push(firstSize) // 子項長度 const size = lazyloadListPartLength const remainder = onSaleLen % size arr.push( ...Array.from({length: Math.floor(onSaleLen/size) - 1}, () => size), ) if (onSaleLen) { arr.push(onSaleLen <= size ? onSaleLen : size + remainder) } // 記錄此時售罄項的索引,因為要點擊展開才能加載售罄列表 const soldOutIndex = arr.length if (soldOutLen) { const remainder = soldOutLen % size arr.push( ...Array.from({length: Math.floor(soldOutLen/size) - 1}, () => size), soldOutLen <= size ? soldOutLen : size + remainder ) } console.log('genSubListSize', arr) return { subSize: arr, soldOutLen, soldOutIndex } } /** * eg: onSaleLen = 25; soldOutLen = 9; size = 5 * return [5, 5, 5, 5, 6, 8] * eg: onSaleLen = 15; soldOutLen = 9; size = 5 * return [5, 5, 6, 8] * eg: onSaleLen = 10; soldOutLen = 10; size = 5 * return [5, 6, 9] * eg: onSaleLen = 14; soldOutLen = 10; size = 5 * return [5, 5, 5, 9] * eg: onSaleLen = 8; soldOutLen = 9; size = 5 * return [9, 8] * eg: onSaleLen = 2; soldOutLen = 10; size = 7 像這種小于非售罄小于size的,只能取到3了 * return [3, 9] **/
現(xiàn)在取列表長度為20,12個非售罄,8個售罄,一屏5個
// pages/third/index const goodsList = Array.from({length: 20}, (item, index) => { return {name: `第${index+1}個`, soldOut: index < 12 ? false : true} }) Page({ // ... onLoad() { this.initObserver() this.handleGoodsList() }, handleGoodsList () { const { onSaleLen, soldOutLen } = this.countSaleLen() console.log('onSaleLen', onSaleLen, 'soldOutLen', soldOutLen); const { subSize, soldOutLen: soldOutLength, soldOutIndex } = this.genSubListSize(onSaleLen, soldOutLen) const renderList = this.genRenderList(subSize) console.log('renderList', renderList); }, countSaleLen () { const soldOutIndex = goodsList.findIndex(good => good.soldOut) if (soldOutIndex === -1) { return { onSaleLen: goodsList.length, soldOutLen: 0 } } return { onSaleLen: soldOutIndex, soldOutLen: goodsList.length - soldOutIndex } }, // 根據(jù)分組數(shù)組生成渲染列表 genRenderList (subSize) { const before = goodsList const after = [] let subList = [] // 二維數(shù)組子項 let subLen = 0 // 子項長度 let splitSizeArrIdx = 0 // 長度數(shù)組索引 for (let i = 0; i < before.length; i++) { if (subLen === subSize[splitSizeArrIdx]) { splitSizeArrIdx++ after.push(subList) subList = [] subLen = 0 } before[i]['part'] = `第${splitSizeArrIdx+1}屏` subList.push(before[i]) subLen++ } // 最后一個子項添加進去 after.push(subList) return after } })
打印一下renderList,得到了動態(tài)切分的數(shù)據(jù)了
列表分組
跑一下demo
當然需求是不斷變化的,下次就不一定是售罄非售罄了,但是萬變不離其宗,再怎么分,把每一項的數(shù)組長度定好之后,再生成渲染的renderList就可以了。所以可以把懶加載的這塊邏輯抽離出來封裝。
demo源碼
參考
到此這篇關于小程序列表懶加載的文章就介紹到這了,更多相關小程序列表懶加載內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JS window對象的top、parent、opener含義介紹
本文為大家介紹下JS window對象的top、parent、opener含義,不了解的朋友可以參考下,希望對大家有所幫助2013-12-12深入淺析javascript立即執(zhí)行函數(shù)
在Javascript中,任何function在執(zhí)行的時候都會創(chuàng)建一個執(zhí)行上下文,因為為function聲明的變量和function有可能只在該function內(nèi)部,這個上下文,在調(diào)用function的時候,提供了一種簡單的方式來創(chuàng)建自由變量或私有子function。2015-10-10JS條形碼(一維碼)插件JsBarcode用法詳解【編碼類型、參數(shù)、屬性】
這篇文章主要介紹了JS條形碼(一維碼)插件JsBarcode用法,較為詳細的分析了條形碼插件JsBarcode編碼類型、參數(shù)、屬性等相關功能、使用方法與注意事項,需要的朋友可以參考下2017-04-04JS實現(xiàn)pasteHTML兼容ie,firefox,chrome的方法
這篇文章主要介紹了JS實現(xiàn)pasteHTML兼容ie,firefox,chrome的方法,涉及javascript針對頁面元素的動態(tài)操作技巧,具有一定參考借鑒價值,需要的朋友可以參考下2016-06-06