Bootstrap

2.9 scroll-view 介绍:如果渲染一个滚动的长列表?(二)

视频链接:

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日