使用 JS 構建跨平臺的原生應用:ListView 組件介紹

jopen 9年前發布 | 30K 次閱讀 ListView 移動Web開發框架 移動開發
 

使用 JS 構建跨平臺的原生應用:ListView 組件介紹

背景

滾動列表幾乎是移動開發中用途最廣的 UI 組件,其重要性不言而喻。由于平臺差異性,React Native 中的滾動列表組件 ListView 并沒有直接映射為 Android 中的 ListView 或 iOS 中的 UITableView,而是在ScrollView 的基礎上使用 JS 做了一次封裝。這樣,滾動體驗部分由 Native 負責,而 React 部分則專注于組件何時渲染、如何渲染等問題。

ListView的基本設計原則是 “數據和展現相隔離” ,如下圖所示。給我們帶來的好處就是,我們只需關注數據的組織方式,任何對數據的操作都會自動渲染出對應的展現。

使用 JS 構建跨平臺的原生應用:ListView 組件介紹

數據

ListView在創建時需要綁定數據源,類似于 Android 中的 Adaptor。數據源內部保存了展現需要的初始數據 _dataBlob ,它是一個純粹的對象或數組。列表可以帶SectionHeader(即列表中某一段的標題部分)也可以不帶,本質上相同。數據源默認的格式有三個維度:

  • 第一個維度是 sectionId ,標識屬于哪一段, 可以手動指定或隱式地使用數組索引或對象的 key 值;
  • 第二個維度是 rowId ,標識某個數據段下的某一個行,同樣可以手動指定或隱式地使用數組索引或對象的 key 值;
  • 第三個維度是具體的數據對象,根據實際的需要而定。

需要注意的是,上面只是 默認的數據格式 ,如果它不符合實際的需求, 完全可以使用自定義的數據結構 。唯一的區別就是需要額外指定給 ListView 數據源中哪些是 id,哪些是 rowData。

使用 JS 構建跨平臺的原生應用:ListView 組件介紹

DataSource 的構造函數接收以下幾個參數:

  • rowHasChanged: 用于在數據變化的時候,計算出變化的部分,在更新時只渲染臟數據;
  • sectionHeaderHasChanged: 同理,在列表帶分段標題時需要實現;
  • getRowData/getSectionHeaderData: 如果遵循默認的數據源格式,這兩個方法就沒有必要實現,用內部默認的即可;而當數據源格式是自定義時,需要手動實現這兩個方法。

如文檔中一般介紹的那樣,DataSource 的初始化一般在 getInitialState 方法中:

 getInitialState: function() {
  var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
  return {
    dataSource: ds.cloneWithRows(['row 1', 'row 2']),
  };
}

在初次見到這種寫法時,心中其實疑惑這里為什么不一次性把整個對象 new 出來,而要拆分為兩步?

首先,如前所述,DataSource 的構造函數里的參數 只有四個 ,并不能直接傳入數據對象。其次,一個頁面中的滾動列表常常要不時修改數據,比如:

  • 邊滾動邊添加列表元素;
  • 展現搜索結果時,當搜索條件變化后,列表對應的數據對象需要重置;
  • 對列表中的結果進行篩選、排序等操作。

在這些情況下,數據源需要做調整,但上述寫法可以使得我們在調整時,無需重新定義 rowHasChanged 等比較函數,只需變化真實的數據對象即可。

展現

基本用法

數據源確定后,下一個工作就是列表的渲染。在渲染時發揮重要作用的是renderRow屬性,它接收數據源中保存的數據對象,并通過返回值確定該行該如何進行展現。我們可以對所有行統一進行展現,也可以根據里面的字段做出不同的展現。在列表包含 sectionHeader 時,還需要實現renderSectionHeader方法。一個簡單的例子如下:

var postDemo = React.createClass({
  getInitialState: function() {
    var ds = new ListView.DataSource({
      rowHasChanged: (r1, r2) => r1 !== r2,
      sectionHeaderHasChanged: (s1, s2) => s1 !== s2
    });
    var dataBlob = {...}
    return {
      dataSource: ds.cloneWithRowsAndSections(dataBlob)
    }
  },
  renderRow: function(rowData, sectionId, rowId) {
    return (<View style={styles.row}>
       <Image source={{uri: 'http:' + rowData.pic}}
      style={styles.image} />
        <View style={styles.rightSection}>
          <Text style={styles.title}>{rowData.title}</Text>
          <Text style={styles.priceInfo}>當前價: ¥{rowData.price}</Text>
        </View>
      </View>);
  },
  renderSectionHeader: function(sectionData, sectionId) {
    return (<View>
      <Text style={styles.sectionHeaderStyle}>{sectionId}</Text>
      </View>);
  },
  render: function() {
    return (
      <ListView
      style={styles.listview}
      dataSource={this.state.dataSource}
      renderSectionHeader={this.renderSectionHeader}
      renderRow={this.renderRow}/>
      );
  }
});

Demo 運行的效果如下:

使用 JS 構建跨平臺的原生應用:ListView 組件介紹

分頁

除了簡單的渲染之外,另外一個要考慮的問題就是 當數據量很大的時候如何分頁加載 。這種情形分兩種情況考慮:

  1. 數據一次性拿到,邊滾動邊加載
  2. 數據不是一次性拿到,而是有可能分屏取數據

對于第一種情況,在 ListView 內部其實已經做了分頁的處理:

  • ListView 內部通過curRenderedRowsCount狀態保存已渲染的行數;
  • 初始狀態下,要加載的數據條數等于initialListSize(默認為 10 條);
  • 在滾動時檢測當前滾動的位置和最底部的距離,如果小于scrollRenderAheadDistance(默認為 1000),就更新curRenderedRowsCount,在它原有值基礎上加pageSize個(默認為 1 條);
  • 由于屬性變化,觸發了 ListView 重新的 render 。在渲染過程中,curRenderedRowsCount起到截斷數據的作用,React 的 diff 算法使得只有新加入的數據才會渲染到了界面上。

整個過程類似于 Web 端懶加載機制,即 每次在和底部的距離達到一個閾值時,加載接下來的 pageSize 個數據

對于第二種情況,ListView 提供了相關的屬性:

  • onEndReachedThreshold,在滾動即將到達底部時觸發;
  • onEndReached,在已經到達底部時觸發;

我們可以在這兩個方法中調用接口去拿數據,取到數據后再更新數據源。

多列

很多頁面中的列表并非單列的,如手淘搜索結果頁里,商品分兩列并排展示。乍一看似乎要做出不少調整,但實際上只通過布局即可達到相關效果。 ListView 并沒有強制要求一個 rowData 在展示時一定要占滿一行,在多列的情況下,我們適時調整每個 rowData 占據的寬度即可。

由于 React Native 使用 Flexbox 進行布局,在實現多列時,主要用到的是flexWrap:wrap屬性:它的效果類似于 float,即水平地排列每一項,當放不下時進行折行處理。在設置每行視圖占據一半寬度后就達到了兩列的效果,多列的類似。

使用 JS 構建跨平臺的原生應用:ListView 組件介紹

具體示例可以參考 ListViewGridLayoutExample

滾動

ListView 只是整合了數據和展現,但實際滾動的功能還是由ScrollView全權負責。ScrollView 實現完全和平臺相關:在 iOS 上,它映射為RCTScrollView;在 Android 上,它映射為RCTScrollView和AndroidHorizontalScrollView。

React Native 讓不同端上的技術融合在了一起,同時也給開發人員提出了更高的要求。以 ScrollView 為例,大量的屬性其實原封不動映射給了 UIScrollView ,這就意味著如果想再深入地研究下去,必須對客戶端相關技術有足夠了解。無論是前端還是客戶端,跳出自己熟悉的那片領域也許才是更進一步的關鍵。

談到滾動,有一點不得不說的就是 列表的無限加載 ,這牽涉到滾動的性能。

Github 上的這個 issue 對此展開了熱烈的討論。其中有人就提到,數據量很大情況下,ListView 在加載時所占用的 CPU 和內存會大大增加,滾動到最后就導致了應用 crash。

為此,ListView 中新添加了一個實驗性的屬性: removeClippedSubviews ,它能在滾動時及時刪掉列表中處于視窗的之外的行,以此達到降低內存消耗的目的。不幸的是,即使設置了這個屬性,程序雖然各項占用減少了不少,但還是沒避 免崩潰的命運。處于好奇,我也在最新版的 ListView 基礎上做了簡單嘗試,不斷加載一個無限大的列表,但并沒有出現崩潰的情況:

  • 即使加載了 3000、4000 行,Android 真機、iOS 真機和 iOS 模擬器上都沒有崩潰;
  • Android 上明顯感到數據加載有 階段性的延時 ,即滾動一定程度后,再次滾動數據始終加載不出來或要等一段時間才加載出來,體驗較差;iOS 相比要流暢的多;

但不崩潰并非最終的目的,很多 React Native 使用者都在試圖改進 ListView 的性能表現,相比于直接使用 Native 端的組件,ListView 性能還是差強人意,有很大優化空間。

總結

ListView 并沒有創造出新的東西,它只是集各家所長,很好地將 React 的視圖渲染和 Native 端很成熟的滾動機制融合在了一起,使用起來和其他組件無差,靜態地定義展現、動態地組織數據,是給人帶來的直觀感受。本文僅對 ListView 基礎用法作了簡要介紹,更為細致的點還是要在實際使用的過程中去發現。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!