ListView 踩过的坑
ListView 组件在 react-native 开发中是会频繁使用的。所以在实际开发时,要实现多选操作时碰到一个坑,虽然使用很奇葩的手段解决了,但并没有从根本上解决遇到的问题。经过别人指点后,终于了解问题产生的原因,记录下这个算得上是基础知识的坑。
其实只有一个问题,但是这里也将 ListView 常用到的功能点列出来。
渲染数据
1 | ; |
有一点需要注意,由于 _renderRow 是我们自己定义的方法,所以在这个函数里面出现的 this 值需要手动绑定,让 _renderRow 里面的 this 指向我们需要的this。而在 render 函数里面, this 值就是我们需要的,所以this._renderRow.bind(this)
就可以了。
如果某个需要绑定 this 值的方法经常用到,可以在构造函数内绑定,这样就不需要每次调用的时候都 .bind(this) 了。
1 | constructor(props){ |
增加数据
可以用 onEndReached 属性,值是一个函数,当触底时会执行该函数,在这个函数内获取数据。也可以用
renderFooter 在列表底部渲染一个按钮,点击才加载数据。
这里采用第二种:
1 | // 渲染底部按钮的函数 |
可以看到这里和之前的思路一样,如果正在加载数据,底部按钮就会显示 loading,加载成功后才显示可点击的按钮。
1 | // 获取更多数据的函数 |
1 | _renderRow(row, sectionId, rowId) { |
最后是给 ListView 组件加上 renderFooter 属性
1 | <ListView |
重点是在 loadMore 方法的这部分代码:
1 | let oldAry = [...this.state.data]; |
即新获取到的数据,如何放到已有的数组中,this.state.data 是保存这前五条数据的数组这个毫无疑问,接下来因为我们不能直接改变 state(需要使用setState),所以不能用 push、unshift,所以这里是先取到 this.state.data,然后使用 ...
展开,新获取到的数组也展开,一起放到数组中,赋给新变量。
删除数据
首先是用来删除的方法:
1 | delete(row) { |
传入 row 作为参数,查询到长按的item在数组中的序列,然后删除,把删除后的数组赋给一个新变量。
可能
1 | let newAry = [ |
这样会更好?暂时都可以,然后添加上长按事件,给每一行添加长按事件:
1 | _renderRow(row) { |
就可以了,这里要注意的是,需要传入一个参数。
选中状态
上面的实现都还算顺利,在实现选中状态,也就是多选功能时踩到了坑。功能点是:一个列表支持多选,然后可以批量删除。先实现选中功能。
选中功能可以用背景色或者图标来区分选中与未选,有两种思路,
- 一个全局数组,选中后将row放到这个数组中,判断row是否在这个数组中来表示是否选中。
- 添加一个字段,这个字段标识是否被选中。
全局数组
先说明,这种方式行不通,或者说很难实现,下面是一步一步尝试到无法实现。
先给初始化 state 添加一个 selectedAry: []
,用来保存选中的 row。然后是 choose 事件:
1 | choose(row) { |
如果点击的这个row已经存在被选择数组中,就移除,思路和长按删除一样;否则就是添加到被选择数组中。
然后给每一行添加上点击事件,就在 onLongPress 下面加上
1 | onPress = {this.choose.bind(this, row)} |
然后找个地方放这个数组的长度,这样就能很直观看到选择的数量。
在 ListView 组件上方放一个 Text 组件。
1 | <Text>{this.state.seletedAry.length+'/'+this.state.data.length}</Text> |
看看效果,的确是能够正确显示出数量了,还需要给一个状态来区分出是否被选择。在每一行添加一个 Text 组件,如果没有被选择,就显示 NO,如果被选择了,就显示 YES。
1 | // 先判断在不在 |
OK,页面能正确显示出 NO 了,点击一下,问题来了,被选择数组的长度+1,但是 NO 并没有变成 YES。原因在于,我们做的这些操作,都没有触发到 ListView 重新渲染,setState 的确会触发 render 函数,但是却不会触发 _renderRow 函数啊,所以这里没有变化。OK,问题既然找到了,解决办法就是在 choose 函数内修改 dataSource,以触发 _renderRow 。
1 | choose(row) { |
拿到旧数组,赋给新数组,这样OK吗,答案是NO。这个地方我的理解是,虽然有新数组了,但是数组里面的对象还是原来的对象,内存地址并没有变化。
举个例子
1 | let ary = [{ |
虽然看起来是修改了 newAry 里面对象的值,实际上原数组 ary 的值也发生了改变。所以我们知道了 ListView 判断数组是否发生了改变,要看里面的元素是否发生了变化,而对象需要内存地址不同,才算发生了变化。
那这里如何解决呢?答案是把对象每个都拷贝:
1 | let oldAry = [...this.state.data]; |
虽然的确是触发了 _renderRow 函数(在_renderRow开始alert可以判断是否触发),但是NO 还是 NO,因为 index 的确是 -1 ,经过拷贝后,每一次看到的数组,和上一次都是不同的,即使如果先拷贝,再放到 selectedAry 中去,只能实现只有一个是 YES,点击了另外的,另一个变为 YES,之前为 YES 的变为了 NO。可以仔细思考为什么,这里放出最后挣扎的代码:
1 | choose(row) { |
新增字段
在从接口返回数据后,手动添加上一个字段来标识是否被选中。
1 | data = data.map(item=> { |
然后页面会根据这个字段显示 YES 或者 NO
1 | // 点击选中 |
判断是否在 selectedAry 数组内可以改成直接判断 row.isCheck
1 | <Text>{row.isCheck ? 'YES' : 'NO'}</Text> |
不过显示已选数量还是需要加上,不过不再使用这个数组来做判断。最终的 choose 代码:
1 | choose(row) { |
总结
数组内如果是对象,要修改对象某个属性的值,一定要使用拷贝,才能够触发_renderRow 函数,重新渲染列表。这个了解了,listView 其实就没什么难点了。这里的选中状态功能,可以拓展出修改列表中的值的功能点。
真总结
放上最终的完整代码:
1 | ; |
然后是 github 地址 https://github.com/ltaoo/react-native-init/blob/master/src/containers/ListViewDemo.js