快應用 list教程

2020-08-10 11:18 更新

了解如何正確使用list,優(yōu)化列表渲染性能,靈活實現(xiàn)需求

通過本節(jié),你將學會:

適用場景


簡單場景

開發(fā)者在頁面中實現(xiàn)長列表或者屏幕滾動等效果時,習慣使用div組件做循環(huán)遍歷

示例如下:

假設開發(fā)者要這樣的效果:一個結(jié)構(gòu)簡單的商品列表

使用 div 組件的代碼如下:

<template>
  <!-- div實現(xiàn) -->
  <div class="tutorial-page">
    <!-- 商品列表 -->
    <block for="productList">
      <div class="content-item" onclick="route($item.url)">
        <image class="img" src="{{$item.img}}"></image>
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
      </div>
    </block>

    <!-- 加載更多,監(jiān)聽通用事件appear,出現(xiàn)時加載更多數(shù)據(jù) -->
    <div class="load-more" onappear="loadMoreData">
      <progress type="circular"></progress>
      <text>加載更多</text>
    </div>
  </div>
</template>

然而,當 DOM 結(jié)構(gòu)復雜時,滾動頁面會出現(xiàn)卡頓現(xiàn)象,因為 Native 無法復用div組件實現(xiàn)的列表元素

為了得到流暢的列表滾動體驗,推薦開發(fā)者使用list組件替代div組件實現(xiàn)長列表布局,因為 Native 會復用相同type屬性list-item

使用 list 組件的代碼如下:

<template>
  <!-- 列表實現(xiàn) -->
  <list class="tutorial-page" onscrollbottom="loadMoreData">
    <!-- 商品列表 -->
    <block for="productList">
      <list-item type="product" class="content-item" onclick="route($item.url)">
        <image class="img" src="{{$item.img}}"></image>
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
      </list-item>
    </block>

    <!-- 加載更多,type屬性自定義命名為loadMore -->
    <list-item type="loadMore" class="load-more">
      <progress type="circular"></progress>
      <text>加載更多</text>
    </list-item>
  </list>
</template>

要實現(xiàn) DOM 片段的復用,要求相同type屬性的 DOM 結(jié)構(gòu)完全相同;所以設置相同type屬性list-item是優(yōu)化列表滾動性能的關鍵

注意:

  • list-item內(nèi)不能再嵌套list
  • list-itemtype屬性為必填屬性
  • list-item內(nèi)部需謹慎使用if指令for指令,因為相同type屬性list-item的 DOM 結(jié)構(gòu)必須完全相同,而使用if指令for指令會造成 DOM 結(jié)構(gòu)差異

提示:

若遇到類似xxx cannot be cast to xxx at ...list的錯誤,請檢查list-item組件是否存在如下情形:

  • 未設置type屬性。解決方案:設置type屬性
  • 內(nèi)部使用了if指令。解決方案:使用show指令代替if指令,或設置不同的type屬性
  • 設置為相同的type屬性,但 DOM 結(jié)構(gòu)不一致。解決方案:設置不同的type屬性

復雜場景

實現(xiàn)簡單的商品列表,了解list組件的基本用法和優(yōu)化性能的關鍵后,接下來通過實現(xiàn)多種列表元素類型的復雜列表,進一步了解list組件

示例如下:

假設開發(fā)者要實現(xiàn)這樣的效果:一個商品列表頁,圖片位于左邊和圖片位于右邊的商品交錯顯示

列表中的列表元素可以分為三類,設置三種不同type屬性list-item。分別為:

  • 圖片在左,文字在右的list-item,type屬性自定義命名為productLeft
  • 圖片在右,文字在左的list-item,type屬性自定義命名為productRight
  • 加載更多的list-itemtype屬性自定義命名為loadMore

示例代碼如下:

<template>
  <!-- list中可以劃分為三種類型的DOM結(jié)構(gòu),對應三種type屬性的list-item -->
  <list class="tutorial-page" onscrollbottom="loadMoreData">
    <block for="{{productList}}">
      <!-- 圖片在左,文字在右的list-item,type屬性自定義命名為productLeft -->
      <list-item type="productLeft" class="content-item" if="{{$idx%2 === 0}}" onclick="route($item.url)">
        <image class="img" src="{{$item.img}}"></image>
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
      </list-item>

      <!-- 圖片在右,文字在左的list-item,type屬性自定義命名為productRight -->
      <list-item type="productRight" class="content-item" if="{{$idx%2 === 1}}" onclick="route($item.url)">
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
        <image class="img" src="{{$item.img}}"></image>
      </list-item>
    </block>

    <!-- 加載更多的list-item,type屬性自定義命名為loadMore -->
    <list-item type="loadMore" class="load-more">
      <progress type="circular"></progress>
      <text>加載更多</text>
    </list-item>
  </list>
</template>

性能優(yōu)化


當 DOM 結(jié)構(gòu)復雜時,為了得到流暢的列表滾動體驗,list組件的性能優(yōu)化必不可缺

list組件的性能優(yōu)化分為精簡DOM層級復用list-item、細粒度劃分list-item、關閉scrollpage四個方面

其中,精簡DOM層級、復用list-item是使用list組件必須遵循的優(yōu)化原則,細粒度劃分list-item、關閉scrollpage適用于部分場景,詳見下文

精簡DOM層級

精簡DOM層級,即減少DOM樹的級數(shù)和分支上的DOM節(jié)點數(shù)。層級越少、數(shù)量越少,布局和繪制就會越快

因此,開發(fā)者需要盡量剔除list中無意義的包裹類標簽和層級

復用list-item

復用list-item,即列表中相同的DOM結(jié)構(gòu)設置為同一type屬性list-item,這是優(yōu)化列表滾動體驗的關鍵

細粒度劃分list-item

細粒度劃分list-item,即列表中相同的DOM結(jié)構(gòu)劃分為盡可能小的列表元素(即list-item

示例如下:

假設開發(fā)者要實現(xiàn)這樣的效果:商品按類別分類,展示多種類別

從業(yè)務角度,可按類別劃分為不同type屬性list-item

然而,當list-item復雜時,會出現(xiàn)卡頓現(xiàn)象。推薦拋開業(yè)務邏輯,劃分為盡可能小的列表元素

示例代碼如下:

<template>
  <list class="tutorial-page" onscrollbottom="loadMoreData">
    <!-- 細粒度劃分list-item -->
    <block for="productList">
      <!-- title -->
      <list-item type="title" if="$item.title" class="title {{$idx>0?'margin-top':''}}">
        <text>{{$item.title}}</text>
      </list-item>
      <!-- banner -->
      <list-item type="banner" if="$item.bannerImg" class="banner">
        <image src="{{$item.bannerImg}}"></image>
      </list-item>
      <!-- productMini -->
      <list-item type="productMini" if="$item.productMini" class="product-mini-wrap">
        <div for="value in $item.productMini" class="product-mini">
          <image src="{{value.img}}" class="product-mini-img"></image>
          <text>{{value.name}}</text>
          <text class="product-mini-brief">{{value.brief}}</text>
          <text class="product-mini-price">{{value.price}}</text>
        </div>
      </list-item>
      <!-- textHint -->
      <list-item type="textHint" if="$item.textHint" class="text-hint">
        <text>{{$item.textHint}} ></text>
      </list-item>
    </block>
    <!-- list底部的加載更多 -->
    <list-item type="loadMore" class="load-more">
      <progress type="circular"></progress>
      <text>加載更多</text>
    </list-item>
  </list>
</template>

關閉scrollpage

list組件支持屬性scrollpage,默認關閉,標志是否將頂部頁面中非list的元素隨list一起滾動。開啟scrollpage會降低list渲染性能

因此,在開發(fā)者開啟scrollpage前,推薦先嘗試將頂部頁面中非list的元素,作為一種或多種type屬性list-item,移入list中,從而達到關閉scrollpage提高渲染性能的目的

示例如下:

假設開發(fā)者要實現(xiàn)這樣的效果:頂部banner,banner下方為常見列表,需要整屏滾動

開發(fā)者一般會將頁面劃分為banner和list兩部分,然后開啟listscrollpage屬性,實現(xiàn)整屏滾動

然而,開啟scrollpage會降低list渲染性能,推薦將頂部banner作為一種特殊type屬性list-item,移入list中,關閉scrollpage

示例代碼如下:

<template>
  <!-- 列表實現(xiàn),監(jiān)聽列表的scrollbottom事件,列表滾動到底部時加載更多數(shù)據(jù) -->
  <list class="tutorial-page" onscrollbottom="loadMoreData">
    <list-item type="banner" class="banner">
      <image src="../../Common/img/demo_large.png"></image>
    </list-item>

    <!-- 商品列表 -->
    <block for="productList">
      <list-item type="product" class="content-item" onclick="route($item.url)">
        <image class="img" src="{{$item.img}}"></image>
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
      </list-item>
    </block>

    <!-- list-item實現(xiàn)的加載更多,type屬性自定義命名為loadMore -->
    <list-item type="loadMore" class="load-more">
      <progress type="circular"></progress>
      <text>加載更多</text>
    </list-item>
  </list>
</template>

list-item懶加載

懶加載,簡稱lazyload,本質(zhì)上是按需加載

在傳統(tǒng)的頁面中,常用lazyload優(yōu)化網(wǎng)頁的性能:

  • 實現(xiàn):不加載全部頁面資源,當資源即將呈現(xiàn)在瀏覽器可視區(qū)域時,再加載資源
  • 優(yōu)點:加快渲染的同時避免流量浪費

在框架中,開發(fā)者也可使用lazyload概念優(yōu)化列表的渲染:

  • 實現(xiàn):提前fetch請求足夠的列表數(shù)據(jù)保存在內(nèi)存變量memList中,當list滾動到底部時,從memList中提取部分數(shù)據(jù)來渲染list-item。當memList中數(shù)據(jù)不足時,提前fetch請求數(shù)據(jù),填充memList
  • 優(yōu)點:每次網(wǎng)絡請求與頁面渲染的數(shù)據(jù)量不一致,減少首屏渲染占用的JS執(zhí)行時間,減少渲染后續(xù)list-item的等待時間

示例如下:

假設開發(fā)者要實現(xiàn)這樣的效果:一個商品列表,每次渲染10個商品

  • 渲染首屏時,請求數(shù)據(jù)保存在內(nèi)存變量memList中,從memList中提取部分數(shù)據(jù)渲染列表
  • 加載更多時,首先檢查memList中是否有足夠數(shù)據(jù),有則直接從memList中提取部分數(shù)據(jù)渲染,而不是才開始網(wǎng)絡請求,減少時間消耗。當memList中數(shù)據(jù)不足時,提前請求數(shù)據(jù)

示例代碼如下:

<template>
  <!-- 列表實現(xiàn),監(jiān)聽列表的scrollbottom事件,列表滾動到底部時加載更多數(shù)據(jù) -->
  <list class="tutorial-page" onscrollbottom="renderMoreListItem">
    <!-- 商品列表 -->
    <block for="productList">
      <list-item type="product" class="content-item">
        <image class="img" src="{{$item.img}}"></image>
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
      </list-item>
    </block>

    <list-item type="loadStatus" class="load-status">
      <progress type="circular" show="{{hasMoreData}}"></progress>
      <text show="{{hasMoreData}}">加載更多</text>
      <text show="{{!hasMoreData}}">沒有更多了~</text>
    </list-item>
  </list>
</template>

<script>
  import {dataComponentListLazyload} from '../../Common/js/data'

  // 模擬fetch請求數(shù)據(jù)
  function callFetch (callback) {
    setTimeout(function () {
      callback(dataComponentListLazyload)
    }, 500)
  }

  // 內(nèi)存中存儲的列表數(shù)據(jù)
  let memList = []

  export default {
    data: {
      productList: [],
      hasMoreData: true,
      // 每次渲染的商品數(shù)
      size: 10,
      // 是否正在fetch請求數(shù)據(jù)
      isLoadingData: false
    },
    onInit () {
      this.$page.setTitleBar({ text: 'list-item懶加載' })
      // 獲取數(shù)據(jù)并渲染列表
      this.loadAndRender()
    },
    /**
     * 請求并渲染
     */
    loadAndRender (doRender = true) {
      this.isLoadingData = true
      // 重新請求數(shù)據(jù)并根據(jù)模式判斷是否需要渲染列表
      callFetch(function (resList) {
        this.isLoadingData = false
        if (!resList) {
          console.error(`數(shù)據(jù)請求錯誤`)
        }
        else if (!resList.length) {
          this.hasMoreData = false
        }
        else {
          memList = memList.concat(resList)
          if (doRender) {
            this._renderList()
          }
        }
      }.bind(this))
    },
    _renderList () {
      // 渲染列表
      if (memList.length > 0) {
        const list = memList.splice(0, this.size)
        this.productList = this.productList.concat(list)
      }
      if (memList.length <= this.size) {
        // 提前請求新的數(shù)據(jù)
        this.loadAndRender(false)
      }
    },
    /**
     * 滑動到底部時加載更多
     */
    renderMoreListItem () {
      if (!this.isLoadingData) {
        this._renderList()
      }
    }
  }
</script>

注意:避免在ViewModeldata屬性中定義memList。因為在ViewModeldata屬性中定義變量會觸發(fā)set/get數(shù)據(jù)驅(qū)動定義,而memList作為暫時保存數(shù)據(jù)的變量,不需監(jiān)聽數(shù)據(jù)變化

效果展示:吸頂


本部分非必讀,旨在為有以下需求之一的開發(fā)者提供參考:

  • 需要判斷頁面滾動位置
  • 需要了解appear事件disappear事件

傳統(tǒng)頁面的實現(xiàn)思路

吸頂是傳統(tǒng)web頁面中的一種比較老的交互方式:

  • 吸頂元素的初始位置一般靠近頁面頂部,但與頂部有一定的距離
  • 當手指向上滑動超過吸頂元素的初始位置時,把吸頂元素固定在頂部
  • 當手指向下滑動到達吸頂元素的初始位置時,取消吸頂元素在頂部的固定

吸頂在傳統(tǒng)web頁面中的實現(xiàn)思路是監(jiān)聽scroll事件,當頁面滾動到一定位置時,做一些事情來改變吸頂元素在窗口中的位置

框架的實現(xiàn)思路

然而,與傳統(tǒng)web頁面不同,在框架中,scroll事件僅適用于list組件,且獲取的值是滾動的相對坐標值,在使用時,需要通過累加來獲取當前滾動位置的絕對坐標

此外,scroll事件在列表滾動時會被高頻觸發(fā),存在潛在性能問題

因此,在框架中,推薦開發(fā)者使用appear事件disappear事件來實現(xiàn)吸頂效果,appear事件在組件出現(xiàn)時觸發(fā),disappear事件在組件消失時觸發(fā)

appear事件disappear事件是組件的通用事件,文檔中標有支持通用事件的組件都支持這兩個事件,包括div組件list-item組件

靈活使用appear事件disappear事件,能實現(xiàn)大部分需要判斷滾動位置的需求

框架的具體實現(xiàn)與代碼

接下來,對應在list組件中實現(xiàn)吸頂效果的示例代碼,具體分析實現(xiàn)思路

首先,了解頂部元素吸頂元素

  • 列表中的頂部元素type屬性toplist-item
  • 列表中的吸頂元素type屬性ceilinglist-item

然后,分析吸頂效果實現(xiàn)方案:

  • 使用stack組件做為整個頁面的容器,stack組件的特性為:每個直接子組件按照先后順序依次堆疊,覆蓋前一個子組件
  • stack組件中增加一個排在最后的子組件,作為mask遮擋之前的子組件,顯示效果為一直固定在頂部,這個mask吸頂元素渲染效果完全一致
  • 吸頂元素需要吸頂時,顯示對應的mask,實現(xiàn)吸頂?shù)男Ч?;?code>吸頂元素不需要吸頂時,隱藏對應的mask

最后,判斷吸頂條件:

  • 當頁面向下滾動到頂部元素消失在視野時,吸頂元素需要固定在頂部,因此,監(jiān)聽頂部元素disappear事件,顯示mask
  • 當頁面向上滾動到頂部元素出現(xiàn)在視野時,吸頂元素需要取消固定,因此,監(jiān)聽頂部元素appear事件,隱藏mask

示例代碼如下:

<template>
  <!-- 利用stack組件,使"列表中的吸頂元素對應的Mask"覆蓋列表 -->
  <stack class="tutorial-page">
    <list class="list">
      <!-- 通過監(jiān)聽"列表中的頂部元素"的元素的appear和disappear事件,控制"列表中的吸頂元素對應的Mask"的顯示 -->
      <list-item type="top" ondisappear="showMask" onappear="hideMask">
        <div class="height-300 bg-blue">
          <text>列表中的頂部元素</text>
        </div>
      </list-item>
      <!-- 列表中的吸頂元素 -->
      <list-item type="ceiling">
        <div class="height-300 bg-red">
          <text>列表中的吸頂元素</text>
        </div>
      </list-item>
      <!-- 普通列表元素 -->
      <list-item for="list" type="common" class="list-item">
        <text class="text">{{$item}}</text>
      </list-item>
    </list>

    <!-- 列表中的吸頂元素對應的Mask -->
    <div show="{{maskShow}}">
      <div class="height-300 bg-red">
        <text>列表中的吸頂元素</text>
      </div>
    </div>
  </stack>
</template>

<style lang="less">
  .tutorial-page {
    flex-direction: column;
    .list {
      width: 750px;
      flex-grow: 1;
      .list-item {
        height: 150px;
        border-bottom-width: 1px;
        border-bottom-color: #0faeff;
        .text {
          flex: 1;
          text-align: center;
        }
      }
    }
    .height-300 {
      height: 300px;
    }
    .bg-red {
      flex-grow: 1;
      justify-content: center;
      background-color: #f76160;
    }
    .bg-blue {
      flex-grow: 1;
      justify-content: center;
      background-color: #0faeff;
    }
  }
</style>

<script>
  export default {
    data: {
      maskShow: false,
      appearCount: 0,
      list: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N']
    },
    onInit(){
      this.$page.setTitleBar({ text: '效果展示:吸頂' })
    },
    showMask () {
      this.maskShow = true
    },
    hideMask () {
      // 加載頁面時,所有元素的appear事件都會被觸發(fā)一次。因此,需要過濾第一次的appear事件
      if (this.appearCount) {
        this.maskShow = false
      } else {
        ++this.appearCount
      }
    }
  }
</script>

總結(jié)


了解list組件的特點,可以更好的提升頁面性能,避免后期開發(fā)過程中引起的性能問題


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號