ListView 长列表

最适用于显示同类的长列表数据类型,对渲染性能有一定的优化效果

代码演示

自定义容器

注意:需要设置 ListView 的 style 的 height/overflow,以此作为滚动容器。

同时建议设置bodyoverflow: hidden

  1. /* eslint no-dupe-keys: 0, no-mixed-operators: 0 */
  2. import { ListView } from 'antd-mobile';
  3. const data = [
  4. {
  5. img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',
  6. title: '相约酒店',
  7. des: '不是所有的兼职汪都需要风吹日晒',
  8. },
  9. {
  10. img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',
  11. title: '麦当劳邀您过周末',
  12. des: '不是所有的兼职汪都需要风吹日晒',
  13. },
  14. {
  15. img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
  16. title: '食惠周',
  17. des: '不是所有的兼职汪都需要风吹日晒',
  18. },
  19. ];
  20. let index = data.length - 1;
  21. const NUM_SECTIONS = 5;
  22. const NUM_ROWS_PER_SECTION = 5;
  23. let pageIndex = 0;
  24. function MyBody(props) {
  25. return (
  26. <div className="am-list-body my-body">
  27. <span style={{ display: 'none' }}>you can custom body wrap element</span>
  28. {props.children}
  29. </div>
  30. );
  31. }
  32. const Demo = React.createClass({
  33. getInitialState() {
  34. const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];
  35. const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];
  36. const dataSource = new ListView.DataSource({
  37. getRowData,
  38. getSectionHeaderData: getSectionData,
  39. rowHasChanged: (row1, row2) => row1 !== row2,
  40. sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
  41. });
  42. this.dataBlob = {};
  43. this.sectionIDs = [];
  44. this.rowIDs = [];
  45. this.genData = (pIndex = 0) => {
  46. for (let i = 0; i < NUM_SECTIONS; i++) {
  47. const ii = (pIndex * NUM_SECTIONS) + i;
  48. const sectionName = `Section ${ii}`;
  49. this.sectionIDs.push(sectionName);
  50. this.dataBlob[sectionName] = sectionName;
  51. this.rowIDs[ii] = [];
  52. for (let jj = 0; jj < NUM_ROWS_PER_SECTION; jj++) {
  53. const rowName = `S${ii}, R${jj}`;
  54. this.rowIDs[ii].push(rowName);
  55. this.dataBlob[rowName] = rowName;
  56. }
  57. }
  58. // new object ref
  59. this.sectionIDs = [].concat(this.sectionIDs);
  60. this.rowIDs = [].concat(this.rowIDs);
  61. };
  62. this.genData();
  63. return {
  64. dataSource: dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),
  65. isLoading: false,
  66. };
  67. },
  68. componentDidMount() {
  69. // you can scroll to the specified position
  70. // this.refs.lv.refs.listview.scrollTo(0, 200);
  71. },
  72. onEndReached(event) {
  73. // load new data
  74. console.log('reach end', event);
  75. this.setState({ isLoading: true });
  76. setTimeout(() => {
  77. this.genData(++pageIndex);
  78. this.setState({
  79. dataSource: this.state.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),
  80. isLoading: false,
  81. });
  82. }, 1000);
  83. },
  84. render() {
  85. const separator = (sectionID, rowID) => (
  86. <div key={`${sectionID}-${rowID}`} style={{
  87. backgroundColor: '#F5F5F9',
  88. height: 8,
  89. borderTop: '1px solid #ECECED',
  90. borderBottom: '1px solid #ECECED',
  91. }}
  92. />
  93. );
  94. const row = (rowData, sectionID, rowID) => {
  95. if (index < 0) {
  96. index = data.length - 1;
  97. }
  98. const obj = data[index--];
  99. return (
  100. <div key={rowID}
  101. style={{
  102. padding: '0.08rem 0.16rem',
  103. backgroundColor: 'white',
  104. }}
  105. >
  106. <h3 style={{ padding: 2, marginBottom: '0.08rem', borderBottom: '1px solid #F6F6F6' }}>
  107. {obj.title}
  108. </h3>
  109. <div style={{ display: '-webkit-box', display: 'flex' }}>
  110. <img style={{ height: '1.28rem', marginRight: '0.08rem' }} src={obj.img} />
  111. <div style={{ display: 'inline-block' }}>
  112. <p>{obj.des}</p>
  113. <p><span style={{ fontSize: '1.6em', color: '#FF6E27' }}>35</span>元/任务</p>
  114. </div>
  115. </div>
  116. </div>
  117. );
  118. };
  119. return (<div style={{ margin: '0 auto', width: '96%' }}>
  120. <ListView ref="lv"
  121. dataSource={this.state.dataSource}
  122. renderHeader={() => <span>header</span>}
  123. renderFooter={() => <div style={{ padding: 30, textAlign: 'center' }}>
  124. {this.state.isLoading ? '加载中...' : '加载完毕'}
  125. </div>}
  126. renderSectionHeader={(sectionData) => (
  127. <div>{`任务 ${sectionData.split(' ')[1]}`}</div>
  128. )}
  129. renderBodyComponent={() => <MyBody />}
  130. renderRow={row}
  131. renderSeparator={separator}
  132. className="fortest"
  133. style={{
  134. height: document.body.clientHeight * 3 / 4,
  135. overflow: 'auto',
  136. border: '1px solid #ddd',
  137. margin: '0.1rem 0',
  138. }}
  139. pageSize={4}
  140. scrollRenderAheadDistance={500}
  141. scrollEventThrottle={20}
  142. onScroll={() => { console.log('scroll'); }}
  143. onEndReached={this.onEndReached}
  144. onEndReachedThreshold={10}
  145. />
  146. </div>);
  147. },
  148. });
  149. ReactDOM.render(<Demo />, mountNode);

body 容器

使用 html 的 body 作为滚动容器

  1. /* eslint no-dupe-keys: 0 */
  2. import { ListView } from 'antd-mobile';
  3. const data = [
  4. {
  5. img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',
  6. title: '相约酒店',
  7. des: '不是所有的兼职汪都需要风吹日晒',
  8. },
  9. {
  10. img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',
  11. title: '麦当劳邀您过周末',
  12. des: '不是所有的兼职汪都需要风吹日晒',
  13. },
  14. {
  15. img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
  16. title: '食惠周',
  17. des: '不是所有的兼职汪都需要风吹日晒',
  18. },
  19. ];
  20. let index = data.length - 1;
  21. const NUM_ROWS = 20;
  22. let pageIndex = 0;
  23. const Demo = React.createClass({
  24. getInitialState() {
  25. const dataSource = new ListView.DataSource({
  26. rowHasChanged: (row1, row2) => row1 !== row2,
  27. });
  28. this.genData = (pIndex = 0) => {
  29. const dataBlob = {};
  30. for (let i = 0; i < NUM_ROWS; i++) {
  31. const ii = (pIndex * NUM_ROWS) + i;
  32. dataBlob[`${ii}`] = `row - ${ii}`;
  33. }
  34. return dataBlob;
  35. };
  36. this.rData = this.genData();
  37. return {
  38. dataSource: dataSource.cloneWithRows(this.rData),
  39. isLoading: false,
  40. };
  41. },
  42. componentDidMount() {
  43. // you can scroll to the specified position
  44. // this.refs.lv.refs.listview.scrollTo(0, 200);
  45. },
  46. onEndReached(event) {
  47. // load new data
  48. console.log('reach end', event);
  49. this.setState({ isLoading: true });
  50. setTimeout(() => {
  51. this.rData = { ...this.rData, ...this.genData(++pageIndex) };
  52. this.setState({
  53. dataSource: this.state.dataSource.cloneWithRows(this.rData),
  54. isLoading: false,
  55. });
  56. }, 1000);
  57. },
  58. render() {
  59. const separator = (sectionID, rowID) => (
  60. <div key={`${sectionID}-${rowID}`} style={{
  61. backgroundColor: '#F5F5F9',
  62. height: 8,
  63. borderTop: '1px solid #ECECED',
  64. borderBottom: '1px solid #ECECED',
  65. }}
  66. />
  67. );
  68. const row = (rowData, sectionID, rowID) => {
  69. if (index < 0) {
  70. index = data.length - 1;
  71. }
  72. const obj = data[index--];
  73. return (
  74. <div key={rowID}
  75. style={{
  76. padding: '0.08rem 0.16rem',
  77. backgroundColor: 'white',
  78. }}
  79. >
  80. <h3 style={{ padding: 2, marginBottom: '0.08rem', borderBottom: '1px solid #F6F6F6' }}>
  81. {obj.title}
  82. </h3>
  83. <div style={{ display: '-webkit-box', display: 'flex' }}>
  84. <img style={{ height: '1.28rem', marginRight: '0.08rem' }} src={obj.img} />
  85. <div style={{ display: 'inline-block' }}>
  86. <p>{obj.des}</p>
  87. <p><span style={{ fontSize: '1.6em', color: '#FF6E27' }}>{rowID}</span>元/任务</p>
  88. </div>
  89. </div>
  90. </div>
  91. );
  92. };
  93. return (
  94. <ListView ref="lv"
  95. dataSource={this.state.dataSource}
  96. renderHeader={() => <span>header</span>}
  97. renderFooter={() => <div style={{ padding: 30, textAlign: 'center' }}>
  98. {this.state.isLoading ? '加载中...' : '加载完毕'}
  99. </div>}
  100. renderRow={row}
  101. renderSeparator={separator}
  102. className="am-list"
  103. pageSize={4}
  104. scrollRenderAheadDistance={500}
  105. scrollEventThrottle={20}
  106. onScroll={() => { console.log('scroll'); }}
  107. useBodyScroll
  108. onEndReached={this.onEndReached}
  109. onEndReachedThreshold={10}
  110. />
  111. );
  112. },
  113. });
  114. ReactDOM.render(<Demo />, mountNode);

标题吸顶(body 容器)

区块标题 “吸顶”(sticky) 功能示例

  1. /* eslint no-dupe-keys: 0 */
  2. import { ListView } from 'antd-mobile';
  3. const data = [
  4. {
  5. img: 'https://zos.alipayobjects.com/rmsportal/dKbkpPXKfvZzWCM.png',
  6. title: '相约酒店',
  7. des: '不是所有的兼职汪都需要风吹日晒',
  8. },
  9. {
  10. img: 'https://zos.alipayobjects.com/rmsportal/XmwCzSeJiqpkuMB.png',
  11. title: '麦当劳邀您过周末',
  12. des: '不是所有的兼职汪都需要风吹日晒',
  13. },
  14. {
  15. img: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
  16. title: '食惠周',
  17. des: '不是所有的兼职汪都需要风吹日晒',
  18. },
  19. ];
  20. let index = data.length - 1;
  21. const NUM_SECTIONS = 5;
  22. const NUM_ROWS_PER_SECTION = 5;
  23. let pageIndex = 0;
  24. const Demo = React.createClass({
  25. getInitialState() {
  26. const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];
  27. const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];
  28. const dataSource = new ListView.DataSource({
  29. getRowData,
  30. getSectionHeaderData: getSectionData,
  31. rowHasChanged: (row1, row2) => row1 !== row2,
  32. sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
  33. });
  34. this.dataBlob = {};
  35. this.sectionIDs = [];
  36. this.rowIDs = [];
  37. this.genData = (pIndex = 0) => {
  38. for (let i = 0; i < NUM_SECTIONS; i++) {
  39. const ii = (pIndex * NUM_SECTIONS) + i;
  40. const sectionName = `Section ${ii}`;
  41. this.sectionIDs.push(sectionName);
  42. this.dataBlob[sectionName] = sectionName;
  43. this.rowIDs[ii] = [];
  44. for (let jj = 0; jj < NUM_ROWS_PER_SECTION; jj++) {
  45. const rowName = `S${ii}, R${jj}`;
  46. this.rowIDs[ii].push(rowName);
  47. this.dataBlob[rowName] = rowName;
  48. }
  49. }
  50. // new object ref
  51. this.sectionIDs = [].concat(this.sectionIDs);
  52. this.rowIDs = [].concat(this.rowIDs);
  53. };
  54. this.genData();
  55. return {
  56. dataSource: dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),
  57. isLoading: false,
  58. };
  59. },
  60. onEndReached(event) {
  61. // load new data
  62. console.log('reach end', event);
  63. this.setState({ isLoading: true });
  64. setTimeout(() => {
  65. this.genData(++pageIndex);
  66. this.setState({
  67. dataSource: this.state.dataSource.cloneWithRowsAndSections(this.dataBlob, this.sectionIDs, this.rowIDs),
  68. isLoading: false,
  69. });
  70. }, 1000);
  71. },
  72. render() {
  73. const separator = (sectionID, rowID) => (
  74. <div key={`${sectionID}-${rowID}`} style={{
  75. backgroundColor: '#F5F5F9',
  76. height: 8,
  77. borderTop: '1px solid #ECECED',
  78. borderBottom: '1px solid #ECECED',
  79. }}
  80. />
  81. );
  82. const row = (rowData, sectionID, rowID) => {
  83. if (index < 0) {
  84. index = data.length - 1;
  85. }
  86. const obj = data[index--];
  87. return (
  88. <div key={rowID}
  89. style={{
  90. padding: '0.08rem 0.16rem',
  91. backgroundColor: 'white',
  92. }}
  93. >
  94. <h3 style={{ padding: 2, marginBottom: '0.08rem', borderBottom: '1px solid #F6F6F6' }}>
  95. {obj.title}
  96. </h3>
  97. <div style={{ display: '-webkit-box', display: 'flex' }}>
  98. <img style={{ height: '1.28rem', marginRight: '0.08rem' }} src={obj.img} />
  99. <div style={{ display: 'inline-block' }}>
  100. <p>{obj.des}</p>
  101. <p><span style={{ fontSize: '1.6em', color: '#FF6E27' }}>35</span>元/任务</p>
  102. </div>
  103. </div>
  104. </div>
  105. );
  106. };
  107. return (
  108. <ListView
  109. dataSource={this.state.dataSource}
  110. renderHeader={() => <span>header</span>}
  111. renderFooter={() => <div style={{ padding: 30, textAlign: 'center' }}>
  112. {this.state.isLoading ? '加载中...' : '加载完毕'}
  113. </div>}
  114. renderSectionHeader={(sectionData) => (
  115. <div>{`任务 ${sectionData.split(' ')[1]}`}</div>
  116. )}
  117. renderRow={row}
  118. renderSeparator={separator}
  119. className="am-list"
  120. pageSize={4}
  121. scrollEventThrottle={20}
  122. onScroll={() => { console.log('scroll'); }}
  123. onEndReached={this.onEndReached}
  124. onEndReachedThreshold={10}
  125. stickyHeader
  126. stickyProps={{
  127. stickyStyle: { zIndex: 999, WebkitTransform: 'none', transform: 'none' },
  128. // topOffset: -43,
  129. // isActive: false, // 关闭 sticky 效果
  130. }}
  131. stickyContainerProps={{
  132. className: 'for-stickyContainer-demo',
  133. }}
  134. />
  135. );
  136. },
  137. });
  138. ReactDOM.render(<Demo />, mountNode);

索引列表

用于通讯薄等场景

  1. /* eslint no-mixed-operators: 0 */
  2. import { province } from 'antd-mobile-demo-data';
  3. import { ListView, List } from 'antd-mobile';
  4. const { Item } = List;
  5. const Demo = React.createClass({
  6. getInitialState() {
  7. const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];
  8. const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];
  9. const dataSource = new ListView.DataSource({
  10. getRowData,
  11. getSectionHeaderData: getSectionData,
  12. rowHasChanged: (row1, row2) => row1 !== row2,
  13. sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
  14. });
  15. const dataBlob = {};
  16. const sectionIDs = [];
  17. const rowIDs = [];
  18. Object.keys(province).forEach((item, index) => {
  19. sectionIDs.push(item);
  20. dataBlob[item] = item;
  21. rowIDs[index] = [];
  22. province[item].forEach(jj => {
  23. rowIDs[index].push(jj.value);
  24. dataBlob[jj.value] = jj.label;
  25. });
  26. });
  27. return {
  28. dataSource: dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),
  29. headerPressCount: 0,
  30. };
  31. },
  32. render() {
  33. return (
  34. <ListView.IndexedList
  35. dataSource={this.state.dataSource}
  36. renderHeader={() => <span>头部内容请自定义</span>}
  37. renderFooter={() => <span>尾部内容请自定义</span>}
  38. renderSectionHeader={(sectionData) => (<div className="ih">{sectionData}</div>)}
  39. renderRow={(rowData) => (<Item>{rowData}</Item>)}
  40. className="fortest"
  41. style={{
  42. height: document.body.clientHeight * 3 / 4,
  43. overflow: 'auto',
  44. }}
  45. quickSearchBarStyle={{
  46. position: 'absolute',
  47. top: 20,
  48. }}
  49. delayTime={10}
  50. delayActivityIndicator={<div style={{ padding: 25, textAlign: 'center' }}>渲染中...</div>}
  51. />
  52. );
  53. },
  54. });
  55. ReactDOM.render(<Demo />, mountNode);

索引列表(标题吸顶)

用于通讯薄等场景 “吸顶”(sticky)

  1. import { province as provinceData } from 'antd-mobile-demo-data';
  2. import { ListView, List, SearchBar } from 'antd-mobile';
  3. const { Item } = List;
  4. const Demo = React.createClass({
  5. getInitialState() {
  6. const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];
  7. const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];
  8. const dataSource = new ListView.DataSource({
  9. getRowData,
  10. getSectionHeaderData: getSectionData,
  11. rowHasChanged: (row1, row2) => row1 !== row2,
  12. sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
  13. });
  14. this.createDs = (ds, province) => {
  15. const dataBlob = {};
  16. const sectionIDs = [];
  17. const rowIDs = [];
  18. Object.keys(province).forEach((item, index) => {
  19. sectionIDs.push(item);
  20. dataBlob[item] = item;
  21. rowIDs[index] = [];
  22. province[item].forEach(jj => {
  23. rowIDs[index].push(jj.value);
  24. dataBlob[jj.value] = jj.label;
  25. });
  26. });
  27. return ds.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs);
  28. };
  29. return {
  30. inputValue: '',
  31. dataSource: this.createDs(dataSource, provinceData),
  32. headerPressCount: 0,
  33. };
  34. },
  35. onSearch(val) {
  36. const pd = { ...provinceData };
  37. Object.keys(pd).forEach((item) => {
  38. pd[item] = pd[item].filter(jj => jj.spell.toLocaleLowerCase().indexOf(val) > -1);
  39. });
  40. this.setState({
  41. inputValue: val,
  42. dataSource: this.createDs(this.state.dataSource, pd),
  43. });
  44. },
  45. render() {
  46. return (<div style={{ paddingTop: '0.88rem', position: 'relative' }}>
  47. <div style={{ position: 'absolute', top: 0, left: 0, right: 0 }}>
  48. <SearchBar
  49. value={this.state.inputValue}
  50. placeholder="搜索"
  51. onChange={this.onSearch}
  52. onClear={() => { console.log('onClear'); }}
  53. onCancel={() => { console.log('onCancel'); }}
  54. />
  55. </div>
  56. <ListView.IndexedList
  57. dataSource={this.state.dataSource}
  58. renderHeader={() => <span>头部内容请自定义</span>}
  59. renderFooter={() => <span>尾部内容请自定义</span>}
  60. renderSectionHeader={(sectionData) => (<div className="ih">{sectionData}</div>)}
  61. renderRow={(rowData) => (<Item>{rowData}</Item>)}
  62. className="am-list"
  63. stickyHeader
  64. stickyProps={{
  65. stickyStyle: { zIndex: 999 },
  66. }}
  67. quickSearchBarStyle={{
  68. top: 85,
  69. }}
  70. delayTime={10}
  71. delayActivityIndicator={<div style={{ padding: 25, textAlign: 'center' }}>渲染中...</div>}
  72. />
  73. </div>);
  74. },
  75. });
  76. ReactDOM.render(<Demo />, mountNode);

ListView长列表 - 图1

API ( 适用平台:WEB、React-Native )

React Native ListView 在 WEB 平台上不被支持的 API 列表:

一般情况下,不支持“平台特有”的API,例如androidendFillColor、iOSalwaysBounceHorizontal。另外,使用 css 代替 react-native 的 style 设置方式。

  • onChangeVisibleRows

  • stickyHeaderIndices

  • ScrollView 组件中不被支持的 API:

    • keyboardDismissMode

    • keyboardShouldPersistTaps

    • onContentSizeChange (可使用onLayout替代)

    • removeClippedSubviews

    • scrollEnabled

    • showsHorizontalScrollIndicator (可使用 css style 替代)

    • showsVerticalScrollIndicator (可使用 css style 替代)

  • View 组件 API: 只支持onLayout

WEB 平台新增API

  • useBodyScroll (boolean, false) - 使用 html 的 body 作为滚动容器

  • stickyHeader (boolean, false) - 固定区块标题到页面顶部 (注意: 设置后会自动使用 html 的 body 作为滚动容器)

    • 开启 sticky 后还可以设置 stickyProps / stickyContainerProps (详见 react-sticky)
  • renderBodyComponent (function, () => React.Element) - 自定义 body 的包裹组件

  • renderSectionBodyWrapper (function, (sectionID: any) => React.Element) - 渲染自定义的区块包裹组件

  • useZscroller (boolean, false) - 使用 zscroller 来模拟实现滚动容器 (可用于一些低端 Android 机上)

    • 注意:开启后useBodyScrollstickyHeader设置会自动被忽略
  • scrollerOptions - 详见 zscroller options

WEB 平台新增 ListView.IndexedList 组件

此组件常用于 “通讯录”/“城市列表” 等场景中,支持索引导航功能。

注意:由于索引列表可以点击任一项索引来定位其内容、即内容需要直接滚动到任意位置,这样就难以做到像 ListView 一样能在滚动时自动懒渲染。目前实现上只支持分两步渲染,能借此达到首屏优先显示目的,但如果列表数据量过大时、整体性能仍会有影响。

  • quickSearchBarTop (object{value:string, label:string}, value/label 默认为'#') - 快捷导航栏最顶部按钮、常用于回到顶部

  • quickSearchBarStyle (object) - quickSearchBar 的 style

  • onQuickSearch (function, (sectionID: any, topId?:any) => void) 快捷导航切换时调用

  • delayTime (number) - 默认 100ms, 延迟渲染时间设置(用于首屏优化,一开始渲染initialListSize数量的数据,在此时间后、延迟渲染剩余的数据项、即totalRowCount - initialListSize

  • delayActivityIndicator (react node) - 延迟渲染的 loading 指示器

常见问题与实现原理

ListView 有三种类型的滚动容器:
  • html 的 body 容器

  • 局部 div 容器 (通过 ref 获取到)

  • 使用 zscroller 的模拟滚动容器

前两种获取到相应元素后,调用 scrollTo 方法、滚动到指定位置;第三种通过 ref 获取到组件对象、再获取到 domScroller 、调用 scrollTo 方法。但滚动到具体什么位置,业务上其实也比较难确定。 另一问题:对 dataSource 对象变化时的处理方式是什么?何时调用 onEndReached 方法? ListView 在 componentWillReceiveProps 里会监听 dataSource 对象的变化,并做一次this._renderMoreRowsIfNeeded() ,由于此时this.state.curRenderedRowsCount === this.props.dataSource.getRowCount()即已经渲染的数据与 dataSource 里已有的数据项个数相同,所以 ListView 认为应该再调用 onEndReached 方法。