小程序列表懶加載的實現(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ù)進行分頁,分批進行渲染。

通過右側面板可以看到,一開始沒有渲染所有節(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ù)組,需要前端轉為二維數(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ù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
小程序開發(fā)中如何使用async-await并封裝公共異步請求的方法
在平常的項目開發(fā)中肯定會遇到同步異步執(zhí)行的問題,這篇文章主要介紹了小程序開發(fā)中如何使用async-await并封裝公共異步請求的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01
基于BootStrap multiselect.js實現(xiàn)的下拉框聯(lián)動效果
當option特別多時,一般的下拉框選擇起來就有點力不從心了,所以使用multiselect是個很好的選擇。在網(wǎng)上找了半天找到了解決方案,具體實現(xiàn)代碼大家參考下本文吧2017-07-07
PNGHandler-借助JS讓PNG圖在IE下實現(xiàn)透明(包括背景圖)
PNGHandler-借助JS讓PNG圖在IE下實現(xiàn)透明(包括背景圖)...2007-08-08
JavaScript mixin實現(xiàn)多繼承的方法詳解
這篇文章主要介紹了JavaScript mixin實現(xiàn)多繼承的方法,結合實例形式分析了mixin多繼承的原理與具體實現(xiàn)技巧,需要的朋友可以參考下2017-03-03
javascript實現(xiàn)網(wǎng)頁背景煙花效果的方法
這篇文章主要介紹了javascript實現(xiàn)網(wǎng)頁背景煙花效果的方法,涉及javascript數(shù)學運算及頁面元素動態(tài)操作的相關技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-08-08
Bootstrap開發(fā)實戰(zhàn)之第一次接觸Bootstrap
Bootstrap開發(fā)實戰(zhàn)之第一次接觸Bootstrap,想要學好一門語言,首先應該進行深入了解,感興趣的小伙伴們可以參考一下2016-06-06

