1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        小程序長(zhǎng)列表優(yōu)化實(shí)踐

        共 27503字,需瀏覽 56分鐘

         ·

        2022-06-30 10:56

        一 前言

        在一些電商的小程序項(xiàng)目中,長(zhǎng)列表是一個(gè)很普遍的場(chǎng)景,在加載大量的列表數(shù)據(jù)的過(guò)程中,可能會(huì)遇到手機(jī)卡頓,白屏等問(wèn)題。也許數(shù)據(jù)進(jìn)行分頁(yè)處理可以防止一次性加載數(shù)據(jù)帶來(lái)的性能影響,但是隨著數(shù)據(jù)量越來(lái)越大,還是會(huì)讓小程序應(yīng)用越來(lái)越卡頓,響應(yīng)速度越來(lái)越慢。這種問(wèn)題不僅僅在小程序上,在移動(dòng)端 h5 項(xiàng)目中同樣存在。

        這個(gè)時(shí)候就需要優(yōu)化長(zhǎng)列表,今天將一起討論一下,長(zhǎng)列表的優(yōu)化方案及其實(shí)踐。

        二 小程序長(zhǎng)列表性能瓶頸

        影響小程序長(zhǎng)列表性能的因素有很多。我們先分析一下小程序長(zhǎng)列表的性能卡點(diǎn)是什么?

        • 元素節(jié)點(diǎn)限制:一個(gè)太大的 WXML 節(jié)點(diǎn)樹(shù)會(huì)增加內(nèi)存的使用,樣式重排時(shí)間也會(huì)更長(zhǎng),影響體驗(yàn)。微信小程序官方建議一個(gè)頁(yè)面 WXML 節(jié)點(diǎn)數(shù)量應(yīng)少于 1000 個(gè),節(jié)點(diǎn)樹(shù)深度少于 30 層,子節(jié)點(diǎn)數(shù)不大于 60 個(gè)。這種現(xiàn)象的本質(zhì)原因是每次 setData 都需要?jiǎng)?chuàng)建新的虛擬樹(shù)、和舊樹(shù) diff 操作耗時(shí)都比較高。

        • 圖片性能和內(nèi)存的影響:長(zhǎng)列表的情況一般會(huì)有大量的圖片,內(nèi)存占用增長(zhǎng),長(zhǎng)列表中的大量圖片會(huì)導(dǎo)致內(nèi)存占用急劇上升,內(nèi)存增長(zhǎng)如果超過(guò)了限制,也會(huì)導(dǎo)致小程序出現(xiàn)白屏或黑屏,甚至整個(gè)小程序發(fā)生閃退。

        • setData 頻率以及數(shù)據(jù)量的影響,長(zhǎng)列表的情況下,會(huì)有很多列表單元項(xiàng),如果每個(gè) item 內(nèi)部會(huì)觸發(fā) setData,會(huì)造成 setData 的頻率急速上升;并且在向每一個(gè) item 注入數(shù)據(jù)的時(shí)候,會(huì)造成數(shù)據(jù)量傳輸過(guò)大,這也是一種性能的開(kāi)銷。而且如果在首次渲染過(guò)程中,加載大量的數(shù)據(jù),就會(huì)造成,首次 setData 的時(shí)候耗時(shí)高。

        那么上述有一個(gè)問(wèn)題,為什么向 item 注入數(shù)據(jù)會(huì)影響性能呢?這些主要是因?yàn)樾〕绦虻脑O(shè)計(jì)。

        整個(gè)小程序框架系統(tǒng)分為兩部分:邏輯層(App Service)和 視圖層(View)。小程序提供了自己的視圖層描述語(yǔ)言 WXML 和 WXSS,以及基于 JavaScript 的邏輯層框架,并在視圖層與邏輯層間提供了數(shù)據(jù)傳輸和事件系統(tǒng),讓開(kāi)發(fā)者能夠?qū)W⒂跀?shù)據(jù)與邏輯。

        2.jpeg

        但是當(dāng)外層向每一個(gè) Item 注入數(shù)據(jù)的時(shí)候,本質(zhì)上是外層邏輯層注入數(shù)據(jù)到視圖層,再?gòu)囊晥D層傳輸?shù)?item 組件的邏輯層。如下圖所示。

        1.jpeg

        解決方案:

        那么如上是造成長(zhǎng)列表性能瓶頸的原因,那么解決手段是什么呢?

        無(wú)論是上述哪種性能卡點(diǎn),本質(zhì)上原因就是 Item 的數(shù)量過(guò)多導(dǎo)致的,在小程序和移動(dòng)端 h5 上,由于滑動(dòng)加載,會(huì)導(dǎo)致數(shù)據(jù)量越來(lái)越大,item 越來(lái)越多,所以 控制 item 數(shù)量 是解決問(wèn)題的關(guān)鍵。

        三 傳統(tǒng)優(yōu)化方案

        通過(guò)上面我們知道了,解決長(zhǎng)列表的手段本身就是控制 item 的數(shù)量,原理就是當(dāng)數(shù)據(jù)填充的時(shí)候,理論上數(shù)據(jù)是越來(lái)越多的,但是可以通過(guò)手段,讓視圖上的 item 渲染,而不在視圖范圍內(nèi)的數(shù)據(jù)不需要渲染,那就不去渲染,這樣的好處有:

        • 由于只渲染視圖部分,那么非視圖部分,不需要渲染,或者只放一個(gè) skeleton 骨架元素展位就可以了,首先這大大減少了元素的數(shù)量,也減少了圖片的數(shù)量,直接減少了應(yīng)用占用的內(nèi)存量,減少了白屏的情況發(fā)生。

        • 由于 item 數(shù)量減少了,減少 diff 對(duì)比的數(shù)量,提升了對(duì)比的效率。

        • 如果 item 里面還有 setData 的操作,那么有間接性減少 setData 的數(shù)量,以及數(shù)據(jù)的傳輸量。

        明白了基本原理之后,接下來(lái)看一下具體的實(shí)現(xiàn)方案。

        1 基于 scroll-view 計(jì)算

        讓視圖區(qū)域的 item 真實(shí)的渲染,這是長(zhǎng)列表優(yōu)化的主要手段,那么第一個(gè)問(wèn)題就是如何知道哪些 item 在可視區(qū)域內(nèi)? 正常情況下,當(dāng)在移動(dòng)端滑動(dòng)設(shè)備的時(shí)候,只有手機(jī)屏幕內(nèi)可視區(qū)域是真正需要渲染的部分,如下所示:

        那就首先就要知道哪些 item 在屏幕區(qū)域內(nèi),一般情況下,這種長(zhǎng)列表都是基于 scroll-view 實(shí)現(xiàn)的,scroll-view 提供了很多回調(diào)函數(shù)可以處理滾動(dòng)期間發(fā)生的事件。比如 scroll,scrolltoupper,scrolltolower 等。

        在 scroll 滑動(dòng)過(guò)程中,可以通過(guò) srollTop 和 scroll-view 的高度,以及每一個(gè) item 的高度,來(lái)計(jì)算哪些 item 是在視圖范圍內(nèi)的。

        下面我們來(lái)簡(jiǎn)單的計(jì)算一下,在視圖區(qū)域內(nèi)的 item 的索引:

        • startIndex:為在視圖區(qū)域內(nèi)的起始索引。
        • endIndex:為在視圖區(qū)域內(nèi)的末尾索引。

        那么計(jì)算流程如下所示:

        5.jpeg
        • 通過(guò) scrollTop 和每個(gè) item 的高度 itemHeight 計(jì)算起始索引 startIndex。
        • 再通過(guò) startIndex 和容器高度 height 計(jì)算出末尾索引 endIndex。

        那么處于 startIndex 和 endIndex 的 item 就是在視圖區(qū)域的 item 。那么通過(guò) slice 截取到列表就是需要渲染的內(nèi)容。

        this.setData({
            renderList:this.data.dataList.slice(startIndex,endIndex)
        })

        但是如果只讓視圖中的 item 進(jìn)行渲染,那么其他 item 的地方如何處理呢,因?yàn)槲覀冃枰?scroll-view 構(gòu)造出真實(shí)滑動(dòng)到當(dāng)前位置的效果。這個(gè)時(shí)候?yàn)榱藙?chuàng)建出 scroll-view 真實(shí)的滑動(dòng)效果,不需要渲染數(shù)據(jù)的地方可以用一個(gè)空的元素占位。

        緩沖區(qū):但是正常情況下,不會(huì)直接將 startIndex 和 endIndex 作為真正渲染的內(nèi)容。因?yàn)榛瑒?dòng)的速度是快速的,以豎直方向上的滑動(dòng)為例子,如果快速上滑或者下滑過(guò)程中,需要觸發(fā) setData 改變渲染的內(nèi)容,那么更新不及時(shí)的情況下,不會(huì)讓用戶看到真實(shí)的列表內(nèi)容,這樣就會(huì)造成一個(gè)極差的用戶體驗(yàn)。

        為了解決這個(gè)問(wèn)題,引出了一個(gè)上下緩沖的概念,就是在渲染真實(shí)的列表 item 的時(shí)候,在滑動(dòng)的兩個(gè)邊界加上一定的緩沖區(qū),在緩沖區(qū)的 item 也會(huì)正常渲染。

        還是以上下滑動(dòng)為例子,我們來(lái)看一下,緩沖區(qū)是如何定義的。

        • 比如在視圖區(qū)域的 item 的起始索引為 startIndex,那么如果留一定緩沖區(qū)的話,那么起始索引就變成了,startIndex - bufferCount(這里直接認(rèn)為 startIndex > bufferCount 的前提下)。
        • 同理視圖區(qū)域 item 的末尾索引為 endIndex,那么需要在 endIndex 基礎(chǔ)上加上緩沖區(qū),所以就變成了 endIndex + bufferCount 。

        所以 [ startIndex - bufferCount , endIndex + bufferCount  ] 為真正的渲染區(qū)間,在這個(gè)區(qū)間內(nèi)部的 item 都會(huì)真實(shí)的渲染。

        如下圖,我們來(lái)看一下在滑動(dòng)過(guò)程中,渲染區(qū)間的變化情況:

        6.jpeg

        對(duì)于 bufferCount ,總結(jié)好處有以下二點(diǎn):

        • 緩沖區(qū)從本質(zhì)上防止在快速上滑或者下滑過(guò)程中,setData 更新數(shù)據(jù)不及時(shí),帶來(lái)不友好的視覺(jué)效果。
        • 有了 bufferCount ,可以讓滑動(dòng)到達(dá)一定長(zhǎng)度再進(jìn)行重新計(jì)算渲染邊界,這樣有效的減少了滑動(dòng)過(guò)程中 setData 的頻率。bufferCount 緩沖數(shù)量越大,那么 setData 頻率就會(huì)越小,但是如果 bufferCount 過(guò)大,就違背了虛擬列表的初衷—減少元素?cái)?shù)量,所以開(kāi)發(fā)者需要合理的控制 bufferCount 的大小,正常情況下,屏幕的一屏或者兩屏為宜。

        2 基于 observer 處理

        小程序提供了 createIntersectionObserver 接口,可以創(chuàng)建  IntersectionObserver 對(duì)象來(lái)判斷元素是否在可視區(qū)域內(nèi)。

        這個(gè) api 一般用于判斷曝光埋點(diǎn),微信官方建議使用節(jié)點(diǎn)布局相交狀態(tài)監(jiān)聽(tīng) IntersectionObserver 推斷某些節(jié)點(diǎn)是否可見(jiàn)、有多大比例可見(jiàn);

        先來(lái)看一下這個(gè)方法的基礎(chǔ)方法:createIntersectionObserver 創(chuàng)建并返回一個(gè) IntersectionObserver 對(duì)象實(shí)例。在自定義組件或包含自定義組件的頁(yè)面中,應(yīng)使用 this.createIntersectionObserver([options]) 來(lái)代替。接下來(lái)看一下 IntersectionObserver 對(duì)象上有哪些方法。

        • IntersectionObserver.relativeTo(string selector, Object margins) 使用選擇器指定一個(gè)節(jié)點(diǎn),作為參照區(qū)域之一。

        • IntersectionObserver.relativeToViewport(Object margins) 指定頁(yè)面顯示區(qū)域作為參照區(qū)域之一。

        • IntersectionObserver.observe(string targetSelector, IntersectionObserver.observeCallback callback) 指定目標(biāo)節(jié)點(diǎn)并開(kāi)始監(jiān)聽(tīng)相交狀態(tài)變化情況。

        • IntersectionObserver.disconnect() 停止監(jiān)聽(tīng)?;卣{(diào)函數(shù)將不再觸發(fā)。

        基礎(chǔ)使用:

        wxml中:

        <!--index.wxml-->
        <view class="container">
          <view class="usermotto" />
          <view id="currentView" >觀察元素節(jié)點(diǎn)</view>
        </view>

        js中:

        Page({
          onLoad() {
            const Observer = wx.createIntersectionObserver(this)
            Observer.relativeToViewport({
               top0,   // 當(dāng)元素到達(dá)頂部觸發(fā)回調(diào)事件
               bottom:0// 當(dāng)元素到達(dá)底部觸發(fā)回調(diào)事件
            }).observe('#currentView',(res)=>{ 
                console.log('元素是否在可視范圍內(nèi)',res.intersectionRatio <= 0 ? '否' : '是'   )
            })
          },
        })

        如上通過(guò) IntersectionObserver 對(duì)象來(lái)監(jiān)聽(tīng)元素的位置,然后可以通過(guò) res.intersectionRatio 判斷元素是否在指定的區(qū)域內(nèi)部。

        8.gif

        實(shí)現(xiàn)原理:

        那么這個(gè)方法,既然能判斷元素的曝光,那么也可以用來(lái)做長(zhǎng)列表優(yōu)化使用。它的實(shí)現(xiàn)原理如下所示:

        7.jpeg

        這種方式可以把數(shù)據(jù)進(jìn)行分組,然后每組創(chuàng)建一個(gè) IntersectionObserver ,當(dāng)分組處于視圖區(qū)域內(nèi)的時(shí)候,才渲染本分組的數(shù)據(jù),那么其他分組沒(méi)有在視圖范圍內(nèi),所以不需要渲染真實(shí)的元素,只需要渲染占位元素或者是骨架節(jié)點(diǎn)就可以了。

        緩沖距離:

        這種實(shí)現(xiàn)方案也會(huì)存在相同的問(wèn)題,就是在快速滑動(dòng)過(guò)程中,如果只選擇上下邊界 top:0 和 bottom:0 ,那么也會(huì)造成滑動(dòng)時(shí)候,渲染不及時(shí)導(dǎo)致無(wú)法看到正常的列表元素的情況發(fā)生。

        為了解決這個(gè)問(wèn)題,那么也會(huì)設(shè)置一定的緩沖距離,這個(gè)一般會(huì)在邊界處入手。比如我們可以設(shè)置當(dāng)列表分組在距離屏幕上邊界和下邊界一屏距離的時(shí)候就觸發(fā)事件,渲染真實(shí)的元素。

        wx.getSystemInfo({
          success(res) => {
            const { windowHeight } = res
            const Observer = wx.createIntersectionObserver(this)
            Observer.relativeToViewport({
              top: windowHeight,  // 距離屏幕頂部一屏距離 
              bottom:windowHeight,// 距離屏幕底部一屏距離
            }).observe('#xxx',(res)=>{ 
                //...
                this.setData({
                  // ...選擇渲染的列表
                })
            }) 
          },
            });

        如上面所示,通過(guò) getSystemInfo 獲取屏幕高度 windowHeight,然后設(shè)置 top ,bottom 為屏幕高度,這樣當(dāng)列表分組處于距離屏幕頂部一屏距離和屏幕底部一屏距離都會(huì)觸發(fā)事件,然后就可以通過(guò) intersectionRatio 判斷當(dāng)前列表分組是消失在視圖區(qū)域,還是進(jìn)入到視圖區(qū)域。

        實(shí)現(xiàn)的原理圖如下:

        8.jpeg

        3 經(jīng)典落地方案

        長(zhǎng)列表解決方案目前已經(jīng)非常成熟了,有很多解決思路,但是本質(zhì)上都是大相徑庭的。這里介紹兩種能夠在實(shí)際開(kāi)發(fā)中落地的技術(shù)方案。

        • 微信小程序官方 recycle-view 方案。
        • Taro 的虛擬列表方案。

        微信 recycle-view 方案

        對(duì)于長(zhǎng)列表方案,微信官方有一套自己的解決方案,就是 recycle-view 。

        核心的思路就是只渲染顯示在屏幕的數(shù)據(jù),基本實(shí)現(xiàn)就是監(jiān)聽(tīng) scroll 事件,并且重新計(jì)算需要渲染的數(shù)據(jù),不需要渲染的數(shù)據(jù)留一個(gè)空的 div 占位元素。

        滾動(dòng)過(guò)程中,重新渲染數(shù)據(jù)的同時(shí),需要設(shè)置當(dāng)前數(shù)據(jù)的前后的 div 占位元素高度,同時(shí)是指在同一個(gè)渲染周期內(nèi)。。

        在滾動(dòng)過(guò)程中,為了避免頻繁出現(xiàn)白屏,會(huì)多渲染當(dāng)前屏幕的前后2個(gè)屏幕的內(nèi)容。

        長(zhǎng)列表組件由2個(gè)自定義組件 recycle-view、recycle-item 和一組 API 組成,對(duì)應(yīng)的代碼結(jié)構(gòu)如下

        ├── miniprogram-recycle-view/
            └── recycle-view 組件
            └── recycle-item 組件
            └── index.js

        包結(jié)構(gòu)詳細(xì)描述如下:

        目錄/文件描述
        recycle-view 組件長(zhǎng)列表組件
        recycle-item 組件長(zhǎng)列表每一項(xiàng) item 組件
        index.js提供操作長(zhǎng)列表數(shù)據(jù)的API

        來(lái)看一下具體的使用:


          1. 安裝組件
        npm install --save miniprogram-recycle-view
        • {
            "usingComponents": {
              "recycle-view""miniprogram-recycle-view/recycle-view",
              "recycle-item""miniprogram-recycle-view/recycle-item"
            }
          }
          1. 在頁(yè)面的 json 配置文件中添加 recycle-view 和 recycle-item 自定義組件的配置
        • <recycle-view batch="{{batchSetRecycleData}}" id="recycleId">
            <view slot="before">長(zhǎng)列表前面的內(nèi)容</view>
            <recycle-item wx:for="{{recycleList}}" wx:key="id">
              <view>
                  <image style='width:80px;height:80px;float:left;' src="{{item.image_url}}"></image>
                {{item.idx+1}}. {{item.title}}
              </view>
            </recycle-item>
            <view slot="after">長(zhǎng)列表后面的內(nèi)容</view>
          </recycle-view>
          1. WXML 文件中引用 recycle-view
        • const createRecycleContext = require('miniprogram-recycle-view')
          Page({
              onReadyfunction({
                  var ctx = createRecycleContext({
                    id'recycleId',
                    dataKey'recycleList',
                    pagethis,
                    itemSize: { // 這個(gè)參數(shù)也可以直接傳下面定義的this.itemSizeFunc函數(shù)
                      width162,
                      height182
                    }
                  })
                  ctx.append(newList)
                  // ctx.update(beginIndex, list)
                  // ctx.destroy()
              },
              itemSizeFuncfunction (item, idx{
                  return {
                      width162,
                      height182
                  }
              }
          })
          1. 頁(yè)面 JS 管理 recycle-view 的數(shù)據(jù)

        通過(guò)這種的優(yōu)缺點(diǎn)是顯而易見(jiàn)的。首先對(duì)于 view 和 item 的結(jié)構(gòu)是清晰的,但是對(duì)于數(shù)據(jù)需要手動(dòng)通過(guò) ctx.append 進(jìn)行追加,而且對(duì)于整個(gè) recycle-view 和 recycle-item 的處理邏輯是要和業(yè)務(wù)層耦合在一起的,這種方式對(duì)于小程序的開(kāi)發(fā)者有一定技術(shù)熟練度的要求。

        當(dāng)然 recycle-view 是基于微信原生小程序?qū)崿F(xiàn)的,所以可以適用于原生小程序,以及基于原生小程序衍變的其他平臺(tái)小程序,比如支付寶小程序,美團(tuán)小程序等。即便是 api 層面不兼容,也可以通過(guò)下載改造的方式,來(lái)應(yīng)用到我們的項(xiàng)目中。

        Taro 虛擬列表方案

        Taro 是多端統(tǒng)一開(kāi)發(fā)的解決方案,可以一套代碼運(yùn)行到移動(dòng) web 端,小程序端,React Native 端,Taro 的實(shí)現(xiàn)原理也如出一轍,比起全量渲染數(shù)據(jù)生成的視圖,Taro 只渲染當(dāng)前可視區(qū)域(visible viewport)的視圖,非可視區(qū)域的視圖在用戶滾動(dòng)到可視區(qū)域再渲染:

        3.jpg

        如上圖就是大致的實(shí)現(xiàn)原理。

        基本使用:

        使用 React/Nerv 我們可以直接從 @tarojs/components/virtual-list 引入虛擬列表(VirtualList)組件:

        import VirtualList from '@tarojs/components/virtual-list'

        以 Taro React 為例子,接下來(lái)看一下 VirtualList 的具體使用:

        function buildData (offset = 0{
          return Array(100).fill(0).map((_, i) => i + offset);
        }

        const Row = React.memo(({ id, index, style, data }) => {
          return (
            <View id={id} className={index % 2 ? 'ListItemOdd: 'ListItemEven'} style={style}>
              Row {index} : {data[index]}
            </View>

          );
        })

        export default class Index extends Component {
          state = {
            data: buildData(0),
          }

          render() {
            const { data } = this.state
            const dataLen = data.length
            return (
              <VirtualList
                height={500} /* 列表的高度 */
                width='100%' /* 列表的寬度 */
                itemData={data} /* 渲染列表的數(shù)據(jù) */
                itemCount={dataLen} /*  渲染列表的長(zhǎng)度 */
                itemSize={100} /* 列表單項(xiàng)的高度  */
              >

                {Row} /* 列表單項(xiàng)組件,這里只能傳入一個(gè)組件 */
              </VirtualList>

            );
          }
        }

        VirtualList 的五個(gè)屬性都是必填項(xiàng)。VirtualList 的數(shù)據(jù)處理,數(shù)據(jù)截取,空白填充都是內(nèi)部實(shí)現(xiàn)的,開(kāi)發(fā)者只需要關(guān)注將 data 數(shù)據(jù)注入到 VirtualList 就可以了。這樣讓虛擬列表使用成本大大降低,也降低了和業(yè)務(wù)的耦合度。

        VirtualList 這種方式是基于 Taro 平臺(tái)開(kāi)發(fā)的,所以它的使用場(chǎng)景就有一定局限性,開(kāi)發(fā)者只能通過(guò) Taro 中使用,比如一些原生小程序,就很不適用了,即便是想要通過(guò)改造源碼的方式來(lái)讓 VirtualList 兼容原生小程序或者其他平臺(tái)的小程序,成本也是巨大的,無(wú)異于重構(gòu)一下項(xiàng)目。

        四 改進(jìn)版優(yōu)化方案

        接下來(lái)我們實(shí)現(xiàn)一個(gè)長(zhǎng)列表組件,選用的是第二種基于 IntersectionObserver 這種方式,我們實(shí)現(xiàn)的這個(gè)長(zhǎng)列表遵循一下原則:

        • 和業(yè)務(wù)低耦合,業(yè)務(wù)只負(fù)責(zé)往長(zhǎng)列表綁定列表數(shù)據(jù)就可以了。列表數(shù)據(jù)是可以追加的。
        • 長(zhǎng)列表組件提供了兩個(gè)抽象節(jié)點(diǎn),一個(gè)是真實(shí)渲染的 item ,一個(gè)是占位的 skeleton。
        • 除此之外,因?yàn)榱斜硪话銜?huì)有頭部和尾部,所以提供兩個(gè)插槽用于占位使用。分別為列表前的 before,和列表后的 after。

        如何使用

        業(yè)務(wù)組件使用:在正式講解之前,先來(lái)看一下長(zhǎng)列表組件是如何使用的:

        業(yè)務(wù)組件 wxml 文件:

        <long-list-view 
        list="{{list}}"
        generic:item="list-item"
        generic:skeleton="list-skeleton"
        />

        可以看到 long-list-view 的 props 只有三個(gè)。

        • list 為輸入給 long-list-view 的數(shù)據(jù)源。
        • generic:item 為抽象節(jié)點(diǎn),指向了 list-item。
        • generic:skeleton 也是抽象節(jié)點(diǎn),指向了 list-skeleton。

        我們看一下業(yè)務(wù)組件的 json 文件:

        "usingComponents": {
          "list-item":"...",     // 業(yè)務(wù)組件每個(gè)卡片組件 item 
          "list-skeleton":"..."// 當(dāng)業(yè)務(wù)組件不渲染時(shí),占位的組件
          "long-list-view":"..." // 長(zhǎng)列表組件
        }

        這里引入了一個(gè)新的概念—抽象節(jié)點(diǎn)。那么我們先來(lái)看看什么是抽象節(jié)點(diǎn)。

        抽象節(jié)點(diǎn)

        有時(shí),自定義組件模板中的一些節(jié)點(diǎn),其對(duì)應(yīng)的自定義組件不是由自定義組件本身確定的,而是自定義組件的調(diào)用者確定的。這時(shí)可以把這個(gè)節(jié)點(diǎn)聲明為“抽象節(jié)點(diǎn)”。

        例如,我們現(xiàn)在來(lái)實(shí)現(xiàn)一個(gè)“選框組”(selectable-group)組件,它其中可以放置單選框(custom-radio)或者復(fù)選框(custom-checkbox)。這個(gè)組件的 wxml 可以這樣編寫(xiě):

        <!-- selectable-group.wxml -->
        <view wx:for="{{labels}}">
          <label>
            <selectable disabled="{{false}}"></selectable>
            {{item}}
          </label>
        </view>

        其中,“selectable”不是任何在 json 文件的 usingComponents 字段中聲明的組件,而是一個(gè)抽象節(jié)點(diǎn)。它需要在 componentGenerics 字段中聲明:

        {
          "componentGenerics": {
            "selectable"true
          }
        }

        通過(guò)上面明白了抽象節(jié)點(diǎn)的使用。那么為什么要用抽象節(jié)點(diǎn)呢?

        • 正常情況下,我們長(zhǎng)列表組件是作為公共組件使用的,這時(shí)就存在一個(gè)問(wèn)題,我們的 item 組件如果不用抽象節(jié)點(diǎn)的情況下,組件 item 是需要注冊(cè)到長(zhǎng)列表組件中去的,那么也就是長(zhǎng)列表本身只能服務(wù)于這一種場(chǎng)景,和業(yè)務(wù)強(qiáng)關(guān)聯(lián)到了一起。
        • 這里可能有的同學(xué)會(huì)想到用 slot 插槽解決,但是 slot 作為 item的話,我們是無(wú)法去循環(huán) slot 插槽的,但是也并不是不能解決,就像微信官方 recycle-view 一樣,可以通過(guò)內(nèi)外層 recycle-view 和 recycle-item 嵌套,然后通過(guò) relations 建立起關(guān)聯(lián),這樣本質(zhì)上需要維護(hù)兩個(gè)公共組件。

        recycle-item

        relations: {
          './recycle-view': {
            type'parent'// 關(guān)聯(lián)的目標(biāo)節(jié)點(diǎn)應(yīng)為子節(jié)點(diǎn)
            linked() {}
          }
        },

        recycle-view

        relations: {
          '../recycle-item/recycle-item': {
              type'child'// 關(guān)聯(lián)的目標(biāo)節(jié)點(diǎn)應(yīng)為子節(jié)點(diǎn)
          }
        }

        言歸正傳,我們主要是通過(guò)抽象節(jié)點(diǎn)實(shí)現(xiàn)的,抽象節(jié)點(diǎn)的注冊(cè)是在業(yè)務(wù)組件中的,但是使用是在公共組件中的,這樣就大大降低和業(yè)務(wù)組件的耦合程度。

        說(shuō)明白了抽象節(jié)點(diǎn),然后我們來(lái)看一下長(zhǎng)列表組件結(jié)構(gòu)。

        ├── long-list-view/
            └── index.js 
            └── index.wxml 
            └── index.json
            └── index.wxss

        首先要在 index.json 聲明當(dāng)前組件和抽象節(jié)點(diǎn)。

        index.json

        {
          "component"true,
          "componentGenerics": {
            "item"true,    //抽象節(jié)點(diǎn) item 
            "skeleton"true //抽象節(jié)點(diǎn) skeleton
          }
        }

        接下來(lái)就是重點(diǎn)看一下 index.js 。從上面我們知道傳入長(zhǎng)列表組件中的數(shù)據(jù) list ,list 是隨著加載數(shù)據(jù)增多,會(huì)越來(lái)越多;同時(shí)還有有一種情況發(fā)生,就是如果 list 變化特別頻繁,那么會(huì)讓長(zhǎng)列表一直觸發(fā) setData 來(lái)執(zhí)行渲染任務(wù),這樣也會(huì)造成卡頓的,那么我們長(zhǎng)列表需要做的事情是:

        • 把 list 規(guī)范化,也就是只處理新增的列表數(shù)據(jù),將它們按照 observer 分組處理。這樣當(dāng)視圖容器滾動(dòng)的時(shí)候,只渲染目標(biāo)范圍內(nèi)的分組數(shù)據(jù)。
        • 第二點(diǎn)就是對(duì)于渲染任務(wù),需要做時(shí)間切片處理,防止 list 變化特別頻繁,造成一直處于 setData 更新,而使得用戶響應(yīng)比較慢。

        時(shí)間切片

        時(shí)間切片的手段通常可以采用 setTimeout 來(lái)模擬實(shí)現(xiàn),比如一次性有 50 個(gè) item 需要渲染,那么 list 每次追加 10 個(gè)item ,這個(gè)時(shí)候就會(huì)讓 setData 在短時(shí)間內(nèi)執(zhí)行 5 次,并且還要有視圖上的響應(yīng),這樣就會(huì)造成性能上和用戶體驗(yàn)上的問(wèn)題。

        那么時(shí)間切片是如何解決這個(gè)問(wèn)題的呢?首先每一次渲染會(huì)創(chuàng)建一個(gè)渲染任務(wù) task,但是并不會(huì)立即執(zhí)行 task,而是把 task 放進(jìn)一個(gè)待渲染的隊(duì)列 renderPendingQueue 中,然后每次執(zhí)行隊(duì)列中的一個(gè)任務(wù),當(dāng)任務(wù)執(zhí)行完畢后,通過(guò) setTimeout 來(lái)在下一次的宏任務(wù)中再次執(zhí)行下個(gè)更新任務(wù)。其原理圖如下所示:

        WechatIMG91.jpeg

        下面我們來(lái)實(shí)現(xiàn)這個(gè)功能:

        Component({
          properties:{
            list: {
              typeArray,
              value: [],
              /* 通過(guò) observer 來(lái)監(jiān)聽(tīng) list 的變化 */
              observer(val, preVal) {
                if (val.length) {
                  const cloneVal = val.slice();
                  /* 找到增量傳入的 list  */
                  cloneVal.splice(0, preVal.length);
                  /* 創(chuàng)建一個(gè)渲染任務(wù) */
                  const task = () => {
                    /* setList 為真正需要執(zhí)行的渲染列表的方法 */
                    this.setList(cloneVal);
                  };
                  /* 把渲染任務(wù)放入到待渲染隊(duì)列中 */
                  this.renderPendingQueue.push(task);
                  /* 開(kāi)始執(zhí)行渲染任務(wù) */
                  this.runRenderTask();
                }
              },
            },
          },
          methods:{
              /* 更新待渲染對(duì)列 */
            runRenderTask() {
              /* 如果沒(méi)有渲染任務(wù)了,或者渲染任務(wù)已經(jīng)開(kāi)始,那么直接返回 */
              if (this.renderPendingQueue.length === 0 || this.isRenderTask) return;
              /* 取出第一個(gè)渲染任務(wù) */
              const current = this.renderPendingQueue.shift();
              /* 開(kāi)始渲染 */
              this.isRenderTask = true;
              /* 執(zhí)行渲染任務(wù) */
              typeof current === 'function' && current();
            },
          },
        })

        這個(gè)的處理邏輯是:

        • 首先通過(guò)對(duì) props 中的 list 屬性進(jìn)行監(jiān)聽(tīng),找到增量傳入的 list ,然后創(chuàng)建一個(gè)渲染任務(wù),把渲染任務(wù)放入到待渲染隊(duì)列中 ,接下來(lái)開(kāi)始執(zhí)行渲染任務(wù)。
        • 在執(zhí)行更新任務(wù)的方法中,如果沒(méi)有渲染任務(wù)了,或者渲染任務(wù)已經(jīng)開(kāi)始,那么直接返回;反之取出第一個(gè)渲染任務(wù),開(kāi)始執(zhí)行。

        從上面可以知道,setList 為真正需要執(zhí)行的渲染列表的方法,我們接著往下看:

        Component({
          //...
          data:{
            /* 待渲染的視圖列表 */
            viewList: [],
            /* 手機(jī)屏幕高度 */
            winHeight0,
            /* 分組索引 */
            groupIndex:0
          },
          lifetimes: {
            ready() {
              wx.getSystemInfo({
                success(res) => {
                  this.setData({
                    /* 獲取屏幕的高度 */
                    winHeight: res.windowHeight,
                  });
                },
              });
              /* 保存所有數(shù)據(jù) */
              this.wholeList = [] 
              /* 記錄分組高度 */
              this.groupHeightArr = []
            }
          },
          methods:{
            /* 裝載數(shù)據(jù) */
            setList(val) {
              const { groupIndex } = this.data;
              const newList = this.data.viewList;
              /* 把數(shù)據(jù)保存到 wholeList 中 */
              this.wholeList[groupIndex] = val;
              /* 插入到視圖列表中 */
              newList[groupIndex] = val;
              this.data.groupIndex++;
              /* 直接渲染最新加入的數(shù)據(jù) */
              this.setData(
                {
                  viewList: newList,
                },
                () => {
                  /* 記錄渲染后的視圖高度 */
                  this.setHeight(groupIndex);
                }
              );
            },
          },
        })

        setList 做的事情很簡(jiǎn)單,將增量的列表數(shù)據(jù)渲染出來(lái),然后開(kāi)始通過(guò) setHeight 將渲染后的數(shù)據(jù)高度記錄下來(lái),這里有的同學(xué)會(huì)問(wèn),為什么要記錄渲染后的數(shù)據(jù)高度,原因很簡(jiǎn)單,如果列表單元項(xiàng) item 在規(guī)定的渲染區(qū)域之外,那么需要 skeleton 占位,但是需要設(shè)置 skeleton 高度,所以需要記錄分組的高度作為占位節(jié)點(diǎn)的高度。

        Component({
          //...
          methods:{
            //...
         ···/* 設(shè)置高度 */
            setHeight(groupIndex) {
              const query = wx.createSelectorQuery().in(this);
              query && query
                .select(`#wrp_${groupIndex}`)
                .boundingClientRect((res) => {
                  /* 記錄分組的高度 */
                  this.groupHeightArr[groupIndex] = res && res.height;
                })
                .exec();
              /* 開(kāi)始監(jiān)聽(tīng)分組變化 */
              this.observePage(groupIndex);
            },
          },
        })

        通過(guò) setHeight 來(lái)記錄高度之后。那么接下來(lái)就需要給當(dāng)前分組創(chuàng)建一個(gè) IntersectionObserver 來(lái)判斷:

        • 如果當(dāng)前分組,在規(guī)定視圖范圍內(nèi),那么渲染真實(shí)的 item 元素。
        • 如果當(dāng)前分組,不在規(guī)定的視圖范圍內(nèi),那么渲染 skeleton 占位節(jié)點(diǎn)。
        Component({
          //...
          methods:{
            //...
             /* 觀察頁(yè)面變化 */
            observePage(groupIndex) {
              wx.createIntersectionObserver(this)
                .relativeToViewport({
                  /* 這里規(guī)定的有效區(qū)域?yàn)閮蓚€(gè)屏幕 */
                  top2 * this.data.winHeight,
                  bottom2 * this.data.winHeight,
                })
                .observe(`#wrp_${groupIndex}`, (res) => {
                  const newList = this.data.viewList;
                  const nowWholeList = this.wholeList[groupIndex];
                  if (res.intersectionRatio <= 0) {
                    console.log('當(dāng)前分組:',groupIndex,'虛擬節(jié)點(diǎn)占位' )
                    /* 如果不在有效的視圖范圍內(nèi),那么不需要渲染真實(shí)的數(shù)據(jù),只需要計(jì)算高度,進(jìn)行占位就可以了。 */
                    const listViewHeightArr = [];
                    const listViewItemHeight = this.groupHeightArr[groupIndex] / nowWholeList.length;

                    for (let i = 0; i < nowWholeList.length; i++) {
                      listViewHeightArr.push({ listViewItemHeight });
                    }
                    newList[groupIndex] = listViewHeightArr;
                  } else {
                    console.log('當(dāng)前分組:',groupIndex,'顯示' )
                    /* 如果在有效的區(qū)域內(nèi),那么直接渲染真實(shí)的數(shù)據(jù)就可以了 */
                    newList[groupIndex] = this.wholeList[groupIndex];
                  }

                  this.setData({
                    viewList: newList,
                  }, () => {
                    this.isRenderTask = false;
                    /* 渲染下一個(gè)更新任務(wù) */
                    this.runRenderTask();
                  });
                });
            },
          },
        })

        看一下 observePage 做了哪些事情?

        • 首先通過(guò) IntersectionObserver 創(chuàng)建一個(gè)觀察者對(duì)象,這里規(guī)定的有效區(qū)域?yàn)閮蓚€(gè)屏幕。
        • 接下來(lái)當(dāng)滑動(dòng)屏幕的時(shí)候,如果不在有效的視圖范圍內(nèi),那么不需要渲染真實(shí)的數(shù)據(jù),只需要計(jì)算高度,進(jìn)行占位就可以了。
        • 如果在有效的區(qū)域內(nèi),那么直接渲染真實(shí)的數(shù)據(jù)就可以了。

        接下來(lái)看一下 wxml 如何處理的:

        <view class="list-view">
           <!-- 列表前自定義插槽 -->
          <slot name="before" />
          <view
            wx:for="{{ viewList }}"
            id="wrp_{{ groupIndex }}"
            wx:for-index="groupIndex"
            wx:for-item="listItem"
            wx:key="index"
          >

            <view
              wx:for="{{ listItem }}"
              wx:for-item="listItem"
              wx:key="index"
            >

              <block wx:if="{{ listItem.listViewItemHeight }}">
                <!-- 不在可視范圍內(nèi) -->
                <view style="height: {{listItem.listViewItemHeight}}px;overflow: hidden">
                  <skeleton/>
                </view>·
              </block>
              <block wx:else>
                <!-- 在可視范圍內(nèi) -->
                <item listItem="{{listItem}}"/>
              </block>
            </view>
          </view>
          <!-- 列表后自定義插槽 -->
          <slot name="after" />
        </view>

        模擬使用:

        接下來(lái)我們做一下模擬:

        <scroll-view 
           scroll-y="{{ true }}" 
           bindscrolltolower="handleScrollLower" 
           style="height:{{winHeight}}px;" 
           lower-threshold="200" 
        >

          <long-list-view 
            list="{{list}}"
            generic:item="item"
            generic:skeleton="skeleton"
          />

        </scroll-view>

        數(shù)據(jù)是模擬的,接下來(lái)看一下整體的效果:

        13.gif

        五 總結(jié)

        本章節(jié)介紹了在小程序端長(zhǎng)列表的性能瓶頸,介紹了常用的解決方案,感興趣的同學(xué)可以試著實(shí)現(xiàn)一下長(zhǎng)列表,也希望做小程序列表優(yōu)化的同學(xué)看到,能夠有一個(gè)啟發(fā)。

        • 微信小程序運(yùn)行時(shí)性能

        • Taro:長(zhǎng)列表渲染(虛擬列表)

        • recycle-view


        瀏覽 102
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            伊人伊人| 夜夜高潮夜夜爽精品欧美做爱 | 日本少妇激三级做爰在线 | 欧美亚洲性爱视频 | 国产又黄又猛视频 | 亚洲精品卡一卡二 | 中国操逼小视频 | 小寡妇高潮了好几次 | 日逼导航| 亚洲天堂第一页 |