小程序列表懶加載的實(shí)現(xiàn)方式
小程序上的列表懶加載
長(zhǎng)列表我們經(jīng)常接觸到,長(zhǎng)列表為什么需要懶加載呢,因?yàn)橐坏╀秩緝?nèi)容多了,渲染引擎就需要更多的時(shí)間去渲染畫(huà)面,這時(shí)可能會(huì)出現(xiàn)頁(yè)面白屏、卡頓等。而用戶(hù)其實(shí)只需要看到視窗內(nèi)的內(nèi)容就可以了,不用一次性把全部?jī)?nèi)容渲染完,所以可以通過(guò)懶加載實(shí)現(xiàn)。
分頁(yè)加載
常見(jiàn)的列表懶加載是和后端一起實(shí)現(xiàn),也就是分頁(yè)加載。前端請(qǐng)求第幾頁(yè)的數(shù)據(jù),后端就返回第幾頁(yè)的數(shù)據(jù)。前端要實(shí)現(xiàn)的交互就是當(dāng)用戶(hù)滑動(dòng)到頁(yè)面底部時(shí),就要請(qǐng)求下一頁(yè)的數(shù)據(jù)。
用scroll事件監(jiān)聽(tīng)

高度示意圖
監(jiān)聽(tīng)scroll事件,事件回調(diào)會(huì)有當(dāng)前元素的滾動(dòng)距離scrollTop,當(dāng)scrollTop+screenHeight等于滾動(dòng)高度scrollHeight時(shí),表示已經(jīng)滑動(dòng)到底部。
在小程序中,Page對(duì)象提供onReachBottomapi
onReachBottom: function() {
// 頁(yè)面觸底時(shí)執(zhí)行
},用IntersectionObserver監(jiān)聽(tīng)
用滾動(dòng)監(jiān)聽(tīng)會(huì)非常耗性能,滾動(dòng)時(shí)頻繁觸發(fā)回調(diào)的,所以會(huì)不斷去計(jì)算判斷。比較好的優(yōu)化方案是IntersectionObserverAPI,當(dāng)IntersectionObserver監(jiān)聽(tīng)的元素與可視區(qū)有相交狀態(tài)時(shí),就會(huì)產(chǎn)生回調(diào),這樣就減少了觸發(fā)的頻率
Page({
onLoad: function(){
wx.createIntersectionObserver().relativeToViewport({bottom: 100}).observe('.target-class', (res) => {
res.intersectionRatio // 相交區(qū)域占目標(biāo)節(jié)點(diǎn)的布局區(qū)域的比例,不等于0時(shí)表示有相交
res.intersectionRect // 相交區(qū)域
res.intersectionRect.left // 相交區(qū)域的左邊界坐標(biāo)
res.intersectionRect.top // 相交區(qū)域的上邊界坐標(biāo)
res.intersectionRect.width // 相交區(qū)域的寬度
res.intersectionRect.height // 相交區(qū)域的高度
})
}
})前端分頁(yè)渲染
上面說(shuō)的都是前端結(jié)合接口的分頁(yè)加載。假如說(shuō)接口沒(méi)有分頁(yè),直接就返回了龐大的數(shù)據(jù)列表。前端如果直接就setData所有數(shù)據(jù),會(huì)渲染很久,其實(shí)復(fù)雜的列表渲染20條的時(shí)候就已經(jīng)很慢了。這個(gè)時(shí)候需要對(duì)已經(jīng)獲取到的數(shù)據(jù)進(jìn)行分頁(yè),分批進(jìn)行渲染。

通過(guò)右側(cè)面板可以看到,一開(kāi)始沒(méi)有渲染所有節(jié)點(diǎn),在滑動(dòng)頁(yè)面過(guò)程中節(jié)點(diǎn)再不斷增加。
直接上代碼
<!-- 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是一個(gè)二維數(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}個(gè)`}
})
})
/**
生成的list是一個(gè)二維數(shù)組
[
[{}, {}],
[{}, {}],
[{}, {}],
...
]
**/
Page({
data: {
goodsList: [],
lazyloadIdx: 0
},
onLoad() {
this.setData({
goodsList: [list[0]],
lazyloadIdx: 1
})
},
// 滑動(dòng)到底部時(shí)往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ù)量,又減少渲染時(shí)間
用IntersectionObserver代替onReachBottom
有很多場(chǎng)景用不了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é)點(diǎn)用來(lái)監(jiān)聽(tīng)
// 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)時(shí)加載下一屏
if (res.intersectionRatio) this.lazyloadNext()
})
},
disconnenct() {
lazyloadOb.disconnenct()
}
})加需求!
后端返回的商品列表只是一個(gè)一維數(shù)組,需要前端轉(zhuǎn)為二維數(shù)組,現(xiàn)在需要每屏的列表長(zhǎng)度為5。
假設(shè)商品列表個(gè)數(shù)為21,那么會(huì)生成二維數(shù)組對(duì)應(yīng)的子項(xiàng)長(zhǎng)度:
// 偽代碼 [5, 5, 5, 5, 1]
我們可以設(shè)定分頁(yè)大小pageSize為5,當(dāng)前分頁(yè)pageNum,然后list.slice(pageNum, pageSize)就能截取對(duì)應(yīng)的數(shù)據(jù),再加入到二維數(shù)組中。
但是產(chǎn)品來(lái)加需求了,商品列表默認(rèn)只展示非售罄商品+一個(gè)售罄商品,其余售罄商品要點(diǎn)擊【查看更多】按鈕才展示
假設(shè)非售罄商品有16個(gè),售罄11個(gè),每屏的列表長(zhǎng)度還是5,那么:
[ 5, 5, 5, // 非售罄 2, // 非售罄+售罄 5, 5 // 售罄 ]
只有兩個(gè)的長(zhǎng)度不大適合再分一屏,所以小于5時(shí),直接跟前面的合并
[ 5, 5, 7, // 非售罄+一個(gè)售罄 5, 5 // 售罄 ]
這個(gè)時(shí)候設(shè)定pageSize就沒(méi)法滿(mǎn)足了,所以要根據(jù)售罄個(gè)數(shù),非售罄個(gè)數(shù)以及一屏長(zhǎng)度,算出長(zhǎng)度數(shù)組,再算出對(duì)應(yīng)的二維數(shù)組
/**
* @desc 生成商品列表的子項(xiàng)長(zhǎng)度
* 展示列表包含售罄的,在非售罄列表最后拼接一個(gè)售罄商品,點(diǎn)擊展開(kāi)再加載售罄
*
* @param {number} onSaleLen 非售罄長(zhǎng)度
* @param {number} soldOutLen 售罄長(zhǎng)度
* @returns { { subSize: Array<number>; soldOutLen: number } }
*/
genSubListSize (onSaleLen, soldOutLen) {
// 有售罄的時(shí)候,放一項(xiàng)售罄到非售罄那邊去
if (soldOutLen) {
soldOutLen-= 1
onSaleLen+=1
}
const arr = []
const lazyloadListPartLength = 5 // 一屏5個(gè)
let firstSize = 0
if (onSaleLen < lazyloadListPartLength*2) {
firstSize = onSaleLen
onSaleLen = 0
} else {
firstSize = lazyloadListPartLength
onSaleLen -= lazyloadListPartLength
}
arr.push(firstSize)
// 子項(xiàng)長(zhǎng)度
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)
}
// 記錄此時(shí)售罄項(xiàng)的索引,因?yàn)橐c(diǎn)擊展開(kāi)才能加載售罄列表
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)在取列表長(zhǎng)度為20,12個(gè)非售罄,8個(gè)售罄,一屏5個(gè)
// pages/third/index
const goodsList = Array.from({length: 20}, (item, index) => {
return {name: `第${index+1}個(gè)`, 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ù)組子項(xiàng)
let subLen = 0 // 子項(xiàng)長(zhǎng)度
let splitSizeArrIdx = 0 // 長(zhǎng)度數(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++
}
// 最后一個(gè)子項(xiàng)添加進(jìn)去
after.push(subList)
return after
}
})打印一下renderList,得到了動(dòng)態(tài)切分的數(shù)據(jù)了

列表分組
跑一下demo

當(dāng)然需求是不斷變化的,下次就不一定是售罄非售罄了,但是萬(wàn)變不離其宗,再怎么分,把每一項(xiàng)的數(shù)組長(zhǎng)度定好之后,再生成渲染的renderList就可以了。所以可以把懶加載的這塊邏輯抽離出來(lái)封裝。
demo源碼
參考
到此這篇關(guān)于小程序列表懶加載的文章就介紹到這了,更多相關(guān)小程序列表懶加載內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
小程序開(kāi)發(fā)中如何使用async-await并封裝公共異步請(qǐng)求的方法
在平常的項(xiàng)目開(kāi)發(fā)中肯定會(huì)遇到同步異步執(zhí)行的問(wèn)題,這篇文章主要介紹了小程序開(kāi)發(fā)中如何使用async-await并封裝公共異步請(qǐng)求的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01
基于BootStrap multiselect.js實(shí)現(xiàn)的下拉框聯(lián)動(dòng)效果
當(dāng)option特別多時(shí),一般的下拉框選擇起來(lái)就有點(diǎn)力不從心了,所以使用multiselect是個(gè)很好的選擇。在網(wǎng)上找了半天找到了解決方案,具體實(shí)現(xiàn)代碼大家參考下本文吧2017-07-07
PNGHandler-借助JS讓PNG圖在IE下實(shí)現(xiàn)透明(包括背景圖)
PNGHandler-借助JS讓PNG圖在IE下實(shí)現(xiàn)透明(包括背景圖)...2007-08-08
JavaScript mixin實(shí)現(xiàn)多繼承的方法詳解
這篇文章主要介紹了JavaScript mixin實(shí)現(xiàn)多繼承的方法,結(jié)合實(shí)例形式分析了mixin多繼承的原理與具體實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-03-03
javascript實(shí)現(xiàn)網(wǎng)頁(yè)背景煙花效果的方法
這篇文章主要介紹了javascript實(shí)現(xiàn)網(wǎng)頁(yè)背景煙花效果的方法,涉及javascript數(shù)學(xué)運(yùn)算及頁(yè)面元素動(dòng)態(tài)操作的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-08-08
Bootstrap開(kāi)發(fā)實(shí)戰(zhàn)之第一次接觸Bootstrap
Bootstrap開(kāi)發(fā)實(shí)戰(zhàn)之第一次接觸Bootstrap,想要學(xué)好一門(mén)語(yǔ)言,首先應(yīng)該進(jìn)行深入了解,感興趣的小伙伴們可以參考一下2016-06-06

