react-native flatlist初探
前言
长列表或者无限下拉列表是最常见的应用场景之一。RN 提供的 ListView 组件,在长列表这种数据量大的场景下,性能堪忧。而在最新的 0.43 版本中,提供了 FlatList 组件,或许就是你需要的高性能长列表解决方案。它足以应对大多数的长列表场景。
使用方法
FlatList 有三个核心属性 data renderItem getItemLayout。它继承自 ScrollView 组件,所以拥有 ScrollView 的属性和方法。
renderItem
和 ListView 的 renderRow 类似,它接收一个函数作为参数,该函数返回一个 ReactElement。函数的第一个参数的 item 是 data属性中的每个列表的数据( Array<object> 中的 object) 。这样就将列表元素和数据结合在一起,生成了列表。这里为了测试性能,放入了一个文本和图片
renderItem({item, index}) {
return <View style={styles.listItem}>
<View style={styles.text}>
<Text >{item.title}</Text>
</View>
<View style={styles.image}>
<Image source={{uri:item.imgsource}} style={styles.image} resizeMode='stretch'></Image>
</View>
</View>;
}
getItemLayout
可选优化项。但是实际测试中,如果不做该项优化,性能会差很多。所以强烈建议做此项优化!如果不做该项优化,每个列表都需要事先渲染一次,动态地取得其渲染尺寸,然后再真正地渲染到页面中。
如果预先知道列表中的每一项的高度(ITEM_HEIGHT)和其在父组件中的偏移量(offset)和位置(index),就能减少一次渲染。这是很关键的性能优化点。
getItemLayout={(data, index) => (
console.log("index="+index),
{length: itemHeight, offset: itemHeight * index, index}
)}
注意,这里有个坑,如果设置了getItemLayout,那么renderItem的高度必须和这个高度一样,否则加载一段列表后就会出现错乱和显示空白。
官方文档介绍加入优化项性能会提高很多,测试的时候发现加不加影响不大,可能是我打开方式不正确,有待后续研究
完整代码如下:
'use strict';
import React, {Component} from 'react';
import {
FlatList,
AppRegistry,
StyleSheet,
Text,
View,
Image,
} from 'react-native';
export default class ViewPager extends Component {
constructor(props) {
super(props);
this.state = {
listData: this.getData(0),
myindex: 1,
};
}
getData(index) {
var list = [];
for (let i = 0; i < 20; i++) {
let imgsource;
if (i % 5 == 0) {
imgsource = 'http://photo.l99.com/bigger/01/1417155508319_k38f29.jpg';
} else if (i % 5 == 1) {
imgsource = 'http://img.tupianzj.com/uploads/allimg/160411/9-1604110SI5.jpg';
} else if (i % 5 == 2) {
imgsource = 'http://img.tupianzj.com/uploads/allimg/160411/9-1604110SH4.jpg';
} else if (i % 5 == 3) {
imgsource = 'http://img.tupianzj.com/uploads/allimg/160411/9-1604110SH6.jpg';
} else if (i % 5 == 4) {
imgsource = 'http://img.tupianzj.com/uploads/allimg/160411/9-1604110SH7.jpg';
}
list.push({title: 'title' + (i + (index * 20)), key: 'key' + (i + (index * 20)), imgsource: imgsource});
}
return list;
}
renderItem({item, index}) {
return <View style={styles.listItem}>
<View style={styles.text}>
<Text >{item.title}</Text>
</View>
<View style={styles.image}>
<Image source={{uri:item.imgsource}} style={styles.image} resizeMode='stretch'></Image>
</View>
</View>;
}
render() {
return (
<View style={styles.view}>
<FlatList
data={this.state.listData}
renderItem={this.renderItem}
onEndReached={()=>{
if(this.state.myindex<2){
// 到达底部,加载更多列表项
this.setState({
listData: this.state.listData.concat(this.getData(this.state.myindex)),
myindex:this.state.myindex+1
});
}
console.log("onEndReached=" + this.state.listData.length);
}}
refreshing={false}
onRefresh={() => {
this.setState({
listData: this.getData(0),
myindex:1,
});
console.log("onRefresh=" + this.state.listData.length);
}}
debug={true}
numColumns={1}
getItemLayout={(data, index) => (
// 120 是被渲染 item 的高度 ITEM_HEIGHT。
console.log("index="+index),
{length: itemHeight, offset: itemHeight * index, index}
)}
ListFooterComponent={this.footerView}
onScroll={this._scrollSinkY}
/>
</View>
)
}
footerView() {
return <View style={{flex:1,height:70,justifyContent:'center',alignItems:'center'}}>
<Text>上啦加载更多</Text>
</View>
}
}
const itemHeight = 200;
const styles = StyleSheet.create({
view: {
flex: 1
},
listItem: {
flexDirection: 'row',
flex: 1,
height: itemHeight,
borderBottomWidth: 1,
borderBottomColor: 'red'
},
image: {
height: 180,
width: 150,
},
text: {
height: 180,
width: 100,
},
});
AppRegistry.registerComponent('ViewPager', () => ViewPager);
另外一个坑,运行的时候,加入上拉加载更多和下拉刷新后,多下拉几次以后,上拉加载更多就不起作用了(触发不了onEndReached方法),有可能是是我打开方式不对,欢迎各位大神指出我代码的问题。
源码分析
FlatList 之所以节约内存、渲染快,是因为它只将用户看到的(和即将看到的)部分真正渲染出来了。而用户看不到的地方,渲染的只是空白元素。渲染空白元素相比渲染真正的列表元素需要内存和计算量会大大减少,这就是性能好的原因。
FlatList 将页面分为 4 部分。初始化部分/上方空白部分/展现部分/下方空白部分。初始化部分,在每次都会渲染;当用户滚动时,根据需求动态的调整(上下)空白部分的高度,并将视窗中的列表元素正确渲染来。
895914763-58d48c7ddb570_articlex.jpeg_usedIndexForKey = false;
const lastInitialIndex = this.props.initialNumToRender - 1;
const {first, last} = this.state;
// 初始化时的 items (10个) ,被正确渲染出来
this._pushCells(cells, 0, lastInitialIndex);
// first 就是 在视图中(包括要即将在视图)的第一个 item
if (!disableVirtualization && first > lastInitialIndex) {
const initBlock = this._getFrameMetricsApprox(lastInitialIndex);
const firstSpace = this._getFrameMetricsApprox(first).offset -
(initBlock.offset + initBlock.length);
// 从第 11 个 items (除去初始化的 10个 items) 到 first 渲染空白元素
cells.push(
<View key="$lead_spacer" style={{[!horizontal ? 'height' : 'width']: firstSpace}} />
);
}
// last 是最后一个在视图(包括要即将在视图)中的元素。
// 从 first 到 last ,即用户看到的界面渲染真正的 item
this._pushCells(cells, Math.max(lastInitialIndex + 1, first), last);
if (!this._hasWarned.keys && _usedIndexForKey) {
console.warn(
'VirtualizedList: missing keys for items, make sure to specify a key property on each ' +
'item or provide a custom keyExtractor.'
);
this._hasWarned.keys = true;
}
if (!disableVirtualization && last < itemCount - 1) {
const lastFrame = this._getFrameMetricsApprox(last);
const end = this.props.getItemLayout ?
itemCount - 1 :
Math.min(itemCount - 1, this._highestMeasuredFrameIndex);
const endFrame = this._getFrameMetricsApprox(end);
const tailSpacerLength =
(endFrame.offset + endFrame.length) -
(lastFrame.offset + lastFrame.length);
// last 之后的元素,渲染空白
cells.push(
<View key="$tail_spacer" style={{[!horizontal ? 'height' : 'width']: tailSpacerLength}} />
);
}
既然要使用空白元素去代替实际的列表元素,就需要预先知道实际展现元素的高度(或宽度)和相对位置。如果不知道,就需要先渲染出实际展现元素,在获取完展现元素的高度和相对位置后,再用相同(累计)高度空白元素去代替实际的列表元素。_onCellLayout 就是用于动态计算元素高度的方法,如果事先知道元素的高度和位置,就可以使用上面提到的 getItemLayout 方法,就能跳过 _onCellLayout 这一步,获得更好的性能。
return (
// _onCellLayout 就是这里的 _onLayout
// 先渲染一次展现元素,通过 onLayout 获取其尺寸等信息
<View onLayout={this._onLayout}>
{element}
</View>
);
...
_onCellLayout = (e, cellKey, index) => {
// 展现元素尺寸等相关计算
const layout = e.nativeEvent.layout;
const next = {
offset: this._selectOffset(layout),
length: this._selectLength(layout),
index,
inLayout: true,
};
const curr = this._frames[cellKey];
if (!curr ||
next.offset !== curr.offset ||
next.length !== curr.length ||
index !== curr.index
) {
this._totalCellLength += next.length - (curr ? curr.length : 0);
this._totalCellsMeasured += (curr ? 0 : 1);
this._averageCellLength = this._totalCellLength / this._totalCellsMeasured;
this._frames[cellKey] = next;
this._highestMeasuredFrameIndex = Math.max(this._highestMeasuredFrameIndex, index);
// 重新渲染一次。最终会调用一次上面分析的源码
this._updateCellsToRenderBatcher.schedule();
}
};
简单分析 FlatList 的源码后,后发现它并没有和 native 端复用逻辑。而且如果有些机器性能极差,渲染过慢,那些假的列表——空白元素就会被用户看到!
实测
性能确实很高,加载图片加文章,很流畅。加载到1000条左右的时候,内存占用大概30M(和图片质量有关系),cpu使用在停止时候0.5%左右,加载时候12%左右。