视频链接:
2.9 scroll-view 介绍:如果渲染一个滚动的长列表?(二)
片1
上节课我们学习了scroll-view组件滚动相关的属性,了解了滚动锚定,这节课我们继续学习这个组件,并着手自定义实现一个下拉刷新功能。
与下拉更新相关的属性:
refresher-enabled、refresher-threshold、refresher-triggered、bindrefresherpulling、bindrefresherrefresh、bindrefresherrestore、bindrefresherabort
片2
我们看一下这些事件名称,就像没有断句读的古文,很不好分辨。如果有小驼峰,或者单词中间有一个连字符、下划线什么的,还好识别一些。像 bindrefresherrefresh 事件。这个事件名称实际上是这样三个单词:bind、refresher、refresh,我第一次看到它时,就错以为是 err-refresh。
前面三个属性,还有后面四个事件,都是与下拉刷新有关的。iPhone 手机一代发布的时候,下拉刷新是一个体验亮点。后来这种设计渐渐成为了手机上刷新功能的设计典范。
refresher-enabled 用于控制是否开启自定义下拉刷新,这个属性默认为 false。refresher-threshold 是触发下拉更新的临界值。向下拉,松手又回去了,列表没有更新,这是没有达到 refresher-threshold 的阀值;达到这个阀值以后,松手之后是「更新中」的状态。
refresher-triggered 这个属性是布尔值,默认为 false。它是为了在更新后,取消下拉更新状态的。当组件处于「下拉更新」状态后,它的值变为 true,此时程序要去做一些异步耗时的事情,例如网络加载,待处理完成了,将这个值设置为 false,这样下拉更新的状态就恢复回去了。
后面四个事件,可以说是自定义实现下拉动画效果的关键。
使用wxs自定义实现下拉刷新
片3
<wxs module="refresh">
...
onPulling: function (e, instance) {
var p = Math.min (e.detail.dy/ 80, 1)
var icon = instance.selectComponent ('#refresherIcon')
icon.setStyle ({
opacity: p,
transform: "rotate (" + (90 + p * 180) + "deg)"
})
var view = instance.selectComponent ('.refresh-container')
view.setStyle ({
opacity: p,
transform: "scale (" + p + ")"
})
if (e.detail.dy >= 80) {
if (pullingMessage == "下拉刷新") {
pullingMessage = "释放更新"
instance.callMethod ("setData", {
pullingMessage
})
}
}
}
}
wxs>
片4
接下来我们就尝试实现自定义下拉刷新。bindrefresherpulling 这个事件,是手指按住了,往下拉的过程中派发的。自定义的动画效果,要在这个事件里处理。上面的动画就是自定义实现的下拉更新动画(见上面效果图),当向下拉动时,区域慢慢的放大,同时箭头图标有一个方向的翻转。
上面这段代码稍微有点复杂,主要干了三件事情:
第1,计算拉到哪里了,占总量 高度 80 的多少。还有,找到 icon 图标,设置它的旋转角度,以这样的方式,实现箭头图标在下拉过程中翻转的效果。
第2,找到下拉动画的容器,设置它的缩放,达到看起来越往下拉、容器越大的缩放效果。
第3,当拉到 refresher-threshold 临界值时,改变下拉更新的提示文本。
这是一段 WXS 代码,是在视图层执行的,在这里基本上可以肆意操作更新视图,而不用担心因更新频繁而导致开销太大影响性能,因为它压根儿就不会更新。在我们的代码里面,之所以用 callMethod 这个方法调用页面主体的 setData 方法,就是为了曲线救国、达到更新视图的目的。
每个 WXS 代码中的事件句柄函数,执行的时侯都有两个参数传递进来:事件对象与当前页面的实例对象。
如果没有这两个参数,这个动画就实现不了。WXS代码,是在视图层中执行的,与 JS文件里中的js代码不是一路的,后者是在逻辑层里执行的。
WXS是 WeXin Script 的简写 ,官方文档里讲,WXS 是一套不一样的脚本语言,它有自己的语法,并且不和 JS 完全一致。我们要充分理解并且相信这句话。
举个例子,在 JS 里面,我们一般使用 let 代替 var 声明变量,这可以避免因变量作用域不合适而产生奇怪的 bug。但是在 WXS 里面,如果我们使用 let 声明变量的话,微信开发者工具立刻就给我们爆出一个奇怪的 bug:代码错乱,无法执行,没有任何其他有效的文本错误提示信息。错误根本就无从查找。
这种错误最让人抓狂,一点线索都没有,根本无从查证。WXS 真的是和 JS 不一样的语言,所以在使用的时候,要特别小心,严格按照官方文档去写,不要想当然地的,以为js可以这样写,wxs也可以这样写,不是的。
onRefresh: function (e, instance) {
// 此时手拉开了,进入了加载中的状态
pullingMessage = "更新中"
instance.callMethod ("setData", {
pullingMessage: pullingMessage,
refresherTriggered: true
})
instance.callMethod ("willCompleteRefresh", {})
}
片5
我们再看一下 bindrefresherrefresh 事件,它是组件进入“更新中”状态时派发的事件。
在这个地方,我们需要一个定时器模拟网络异步加载,但是 WXS 里没有定时器,它只有页面实例对象的requestAnimationFrame 函数。要么使用这个requestAnimationFrame 方法模拟一个定时器,要么在 JS 中实现。
我觉得后面这个方法比较简单,我选择了后者。并且呢,我们在之前查看微信团队编写的wxml-to -canvas的源码时,发现他们在写组件时,也同时写了js代码,并不是完全是在wxs脚本中实现的。还有,完全在wxs中实现,这种方式是比较困难的。
我在 JS 代码中定义了一个 willCompleteRefresh 方法,然后再在 WXS 里面,在合适的时机通过 callMethod 调用它(调用代码见上面)。
willCompleteRefresh (){
let intervalId = setInterval (()=>{
let pullingMessage = this.data.pullingMessage
console.log (pullingMessage,pullingMessage == ' 更新中 ')
if (pullingMessage.length < 7){
pullingMessage += '.'
} else {
pullingMessage = ' 更新中 '
}
this.setData ({
pullingMessage
})
},500)
setTimeout (()=>{
clearInterval (intervalId)
this.setData ({
pullingMessage:"已刷新",
refresherTriggered:false,
})
},2000)
}
片6
在这个js函数willCompleteRefresh中,主要做了两件事:一,使用一个定时器模拟实现“更新中...”后面的点点点跳动的动画;二,通过一个延时定时器,在2秒以后设置刷新完成。
看完了这个bindrefresherrefresh事件,我们再看一下其它的事件,bindrefresherrestore 事件,是状态恢复了,是设置了 refresher-triggered 为 false,动画完成以后派发的事件。
bindrefresherabort 是下拉行为被打断时派发的事件,正常情况下这种事件不会收到。
(可以在本课视频中查看示例及运行效果)
mescroll:https://github.com/mescroll/mescroll
minirefresh:https://github.com/minirefresh/minirefresh
片7
关于下拉刷新的组件,有两个开源项目(见上面网址),推荐给大家,有时间值得读一读它们的源码。
最佳实践
片8
接下来我们看一看最佳实践,关于scroll-view这个组件,有以下几点值得注意:
第1,启用 scroll-anchoring属性时,同时添加 overflow-anchor:auto 样式,应对 Android 机型不兼容的情况。
第2,任何时候只开启一个方向的滚动,scroll-y 或 scroll-x 只取其一。
当开启 scroll-y 时,必须给组件一个高度,例如 400px,子组件高度之和一定要大于这个高度。
当启用 scroll-x 时,必须给组件一个宽度,一般这个值是 100%,等于屏宽,子组件宽度之和要大于屏宽。
white-space:nowrap;
display:inline-block
片9
在启用 scroll-x 时,宽度为 100%,如果出现不滚动的现象,可以尝试给滚动容器添加两个这样的样式(见上面代码)。这两个样式的作用,是不换行,并且指定子元素是行内块元素,目的是为了让子元素在横向上排列成一行。
第3,开启 enable-flex,这个属性是在scroll -view 组件上启用 flex 布局的,相当于添加 display:flex 样式。但是如果是自己添加的话,是加在了外围容器上,只有通过这个属性添加,才能加到内围真正的容器上。
片10
第4,如果需要,使用 refresher-enabled 启用下拉动画的自定义。自定义可以很方便的实现一些有创意的交互效果。例如小人跑动这样的加载动画(见上面图片)。
在自定义下拉动画时,下拉动画容器的 slot属性要标记为 refresher,虽然官方文档没有这样写,但如果你不这样做,你的自定义下拉动画可能就不工作了。
第5,下拉动画组件的背景色要用 #F8f8f8,前景色 —— 包括图标与文本,用 #888888这个颜色,这符合微信的设计规范。
第6,尽量不要在 JS 代码里面,在 scroll 事件的句柄函数中,直接更新视图,把相关的频繁的更新视图的代码,放在 WXS 模块中。在大列表视图中尤其要这样做。
接下来我们看一看开发中经常遇到哪些问题。
(一)使用 scroll-view 时,如何优化使用 setData 向其传递大数据、渲染长列表?
片11
// 更新二维数组
const updateList = `tabs [${activeTab}].list [${page}]`
const updatePage = `tabs [${activeTab}].page`
this.setData ({
[updateList]: res.data,
[updatePage]: page + 1
})
<view wx:for="{{gameListWrap}}" wx:for-item="gameList">
...
view>
片12
这是开发者社区上的一个问题,原问题是这样的:“scroll-view 追加数据会自动回到顶部,请问怎样解决?”
在上面代码中,作者可能是想实现一个多 tab 页的功能,数据是 tabs,这是一个大数组。gameListWrap 应该是对这个 tabs 子数据访问的再封装。
在 JS 代码中,${activeTab}与${page} 都是模板字符串中的变量。updateList、updatePage 是 setData 更新时用的 key,因为是变量,所以在使用时一定要用方括号 [] 括起来。
let tabData = this.data.tabs [activeTab]
tabData.list.push (res.data)
tabData.page = page+1
let key = `tabs [${activeTab}]`
this.setData ({
[key]: tabData
})
片13
作者为什么不直接使用 push 方法呢?当有新数据进来时,直接往某个 tab 页数据的底部推入新数据不就可以了吗?(见上面代码)
但这种操作有一个问题。setData 受限于视图层与逻辑层之间用于传话的 evaluateJavascript 函数,每次携带的数据大小,官方要求,在文本序列化以后,大小不能超过 256KB。如果某个 tab 页是一个瀑布流,他 tabData.list 可能是一个越来越大的数据,很有可能会超过 256KB。
const updateListStr = `gameListData [${activeTab}][${page}]`
const updatePageStr = `pages [${activeTab}]`
this.setData ({
[updateListStr]: res,
[updatePageStr]: page + 1
})
使用recycle-view扩展组件:
https://developers.weixin.qq.com/miniprogram/dev/extended/component-plus/recycle-view.html
片14
那么这个问题如何解决呢?可以参照作者在实践过程中找到的解决方法(见上面代码):
将 tab 数据与页面数据分开。在当前页面循环渲染时,按照 pages [activeTab].page 的数字循环;取数据时,依照 page 当前的值,从 gameListData [activeTab] 中查取。gameListData 此时在形式上是一个数组,但实际上相当于是一个 map。
<recycle-view height="200" batch="{{batchSetRecycleData}}" id="recycleId" batch-key="batchSetRecycleData" style="background:white;">
<recycle-item wx:for="{{recycleList}}" wx:key="index" class='item'>
<view>
{{item.id}}: {{item.name}}
view>
recycle-item>
recycle-view>
var ctx = createRecycleContext({
id: 'recycleId',
dataKey: 'recycleList',
page: this,
itemSize: {
width: rpx2px(650),
height: rpx2px(100)
}
})
let newList = []
...
ctx.append(newList)
片15
另外,在渲染长列表的时候,微信在weui扩展组件库中给出了一个长列表组件 recycle-view(见上面网址),它用于渲染无限长的列表(效果见上面)。
这个长列表组件的实现原理也很简单,通过监听 scroll 事件,只渲染当前视图窗口内的 list 列表,看不见的地方用空的占位符代替。
在使用recycle-view这个扩展组件时,batch属性的值必须为,这是由组件自动管理的,不用管他。在js代码中,调用createRecycleContext的时候,传入的dataKey,是recycleList,这个名称必须与wxml中wx:for指定的数据名称一致。如果一个页面中还使用了另一个长列表,则需要再换一个名字,如果只使用一个长列表,名字不用修改。
(可以从本课视频中查看运行效果)
当从后端拉取大数据渲染长列表时,现在你明白应该怎么做了吗?
大多数情况下,卡顿并不定是手机真的卡了,同时间如果我们打开其它原生App,同样是很流畅的。很可能只是视图渲染不及时。
影响小程序渲染效率的罪魁祸首是底层的 evaluateJavascript 这个通讯函数,它可以说是逻辑层与视图层之间一个很小的独立桥,无法承接过大的数据量。所以我们要尽量减少大数据渲染,在视图中的互动操作,要尽量在wxs代码中完成。
(二)如何实现购物类小程序分类选物品的页面?
片16
片17
依据上面效果图来看,这里主要需要实现两个功能:
第1,单击左侧菜单,右侧区域自动滚动到相应位置。
第2,在右侧滚动,左侧菜单自动同步选择并高亮显示。
<scroll-view scroll-y='true' class='nav'>
<view wx:for='{{list}}' wx:key='{{item.id}}' id='{{item.id}}'
class='navList {{currentIndex==index?"active":""}}' bindtap="menuListOnClick" data-index='{{index}}'>{{item.name}}view>
scroll-view>
<scroll-view scroll-y='true' scroll-into-view='{{activeViewId}}' bindscroll='scrollFunc'>
<view class="fishList" wx:for='{{content}}' id='{{item.id}}' wx:key='{{item.id}}'>
<p>{{item.name}}p>
view>
scroll-view>
片18
第一个功能点,可以通过 scroll-into-view 属性实现,将左侧菜单与右侧每块区域的 id 对应起来,单击时更新 scroll-into-view 绑定的 id(见上面代码)。
// 单击左侧菜单
menuListOnClick:function (e){
let me=this;
me.setData ({
activeViewId:e.target.id,
currentIndex:e.target.dataset.index
})
}
// 滚动时触发,计算当前滚动到的位置对应的菜单是哪个
scrollFunc:function (e){
this.setData ({
scrollTop:e.detail.scrollTop
})
for (let i = 0; i < this.data.heightList.length; i++) {
let height1 = this.data.heightList [i];
let height2 = this.data.heightList [i + 1];
if (!height2 || (e.detail.scrollTop >= height1 && e.detail.scrollTop < height2)) {
this.setData ({
currentIndex: i
})
return;
}
}
this.setData ({
currentIndex: 0
})
}
片19
第二个功能点,是通过计算实现的。在列表数据绑定的时候,把右侧每块物品区域的高度记录下来,就是上面代码中的 heightList。右侧列表滚动时,通过绑定 scroll 事件,拿到 scrollTop,循环对比滚动到了哪个区域,就把哪个区域对应的菜单高亮起来(见上面代码)。
课后作业
片20
这节课我们留一个作业:weui组件库中有一个vtabs组件,它实现的效果和本节课最后一个问题实现的效果是类似的,是一个有侧边栏分类的浏览组件,请你尝试在小程序项目中使用一下这个vtabs组件。
好,今天的课就讲到这里。今天主要讲了scroll-view关于实现下拉刷新的相关属性,以及如何使用wxs脚本自定义实现一个下拉更新的效果,还有如何渲染长列表等等,下节课我们学习滚动选择器组件,尝试自定义实现一个省市区多级联动选择器。
2020年5月22日