背景
在小程序开发中,用户交互效果如页面元素拖动可能会因频繁的事件通信导致卡顿。事件处理流程通常如下:
- 事件传递:
touchmove
事件从视图层(Webview)传递到逻辑层(App Service)。 - 事件处理:逻辑层处理事件并通过
setData
更新组件位置。 - 渲染:更新触发渲染,并可能导致脚本执行阻塞,影响用户体验。
这种方式的主要问题在于频繁的层间通信和渲染操作,导致动画和交互过程的延迟。
实现方案
为了减少通信次数并提升响应速度,可以将事件处理移至视图层(Webview)。这可以通过使用 WXS 函数实现,WXS 允许在视图层直接响应事件,减少了逻辑层和视图层之间的交互。
WXS函数
WXS 函数能够处理内置组件的事件,并允许对组件样式和类
进行设置,但不支持自定义组件事件。基本用法如下:
定义WXS函数
var wxsFunction = function(event, ownerInstance) {
var instance = ownerInstance.selectComponent('.classSelector'); // 获取组件实例
instance.setStyle({"font-size": "14px"}); // 设置样式
instance.getDataset(); // 获取数据集
instance.setClass(className); // 设置类名
return false; // 阻止事件冒泡
}
在 WXS 函数中,event
参数包含事件对象,并新增 event.instance
表示触发事件的组件实例。ownerInstance
是触发事件的组件所在的组件实例或页面实例。
使用WXS
-
在 WXML 中引用WXS函数:
<wxs module="test" src="./test.wxs"></wxs> <view change:prop="{{test.propObserver}}" prop="{{propValue}}" bindtouchmove="{{test.touchmove}}" class="movable"></view>
change:prop
用于在prop
属性变化时触发 WXS 函数,bindtouchmove
用于处理touchmove
事件。 -
在WXS文件中定义函数:
module.exports = { touchmove: function(event, instance) { console.log('log event', JSON.stringify(event)); }, propObserver: function(newValue, oldValue, ownerInstance, instance) { console.log('prop observer', newValue, oldValue); } }
touchmove
处理触摸移动事件,propObserver
在prop
属性变化时触发。
场景应用
移动视图
<!-- pages/movable/movable.wxml -->
<wxs module="test" src="./movable.wxs"></wxs>
<view wx:if="{{show}}" class="area" style='position:relative;width:100%;height:100%;'>
<view data-index="1" data-obj="{{dataObj}}" bindtouchstart="{{test.touchstart}}" bindtouchmove="{{test.touchmove}}" bindtouchend='{{test.touchmove}}' class="movable" style="position:absolute;width:100px;height:100px;background:red;left:{{left}}px;top:{{top}}px"></view>
</view>
在 WXML 文件中,定义了一个 view
组件,包含一个可以拖动的 movable
视图。通过 bindtouchstart
、bindtouchmove
和 bindtouchend
绑定了 WXS 文件中定义的事件处理函数。这些函数将处理视图的拖动逻辑,并实时更新其位置。
// movable.wxs
var startX = 0
var startY = 0
var lastLeft = lastTop = 50
function touchstart(event, ins) {
var touch = event.touches[0] || event.changedTouches[0]
startX = touch.pageX
startY = touch.pageY
ins.callMethod('testCallmethod', {
complete: function(res) {
console.log('args', res)
}
})
}
function touchmove(event, ins) {
var touch = event.touches[0] || event.changedTouches[0]
var pageX = touch.pageX
var pageY = touch.pageY
var left = pageX - startX + lastLeft
var top = pageY - startY + lastTop
startX = pageX
startY = pageY
lastLeft = left
lastTop = top
ins.selectComponent('.movable').setStyle({
left: left + 'px',
top: top + 'px'
})
}
module.exports = {
touchstart: touchstart,
touchmove: touchmove,
}
在 WXS 文件中,定义了两个函数:touchstart
和 touchmove
。touchstart
函数记录了触摸的起始位置,并通过 callMethod
调用了逻辑层的 testCallmethod
方法。touchmove
函数根据触摸的移动计算视图的新位置,并使用 selectComponent
和 setStyle
方法更新视图的位置。通过这些操作,视图能够实时响应拖动手势,实现流畅的用户交互体验。
侧边菜单滑动
<!-- page/one/index.wxml -->
<wxs module="test" src="./sidebar.wxs"></wxs>
<view class="page">
<view class="page-bottom">
<view class="page-content">
<view class="wc">
<text>第一个item1</text>
</view>
<view class="wc">
<text>第二个item2</text>
</view>
<view class="wc">
<text>第三个item3</text>
</view>
<view class="wc">
<text>第四个item4</text>
</view>
</view>
</view>
<view data-width="{{windowWidth}}" bindtouchmove="{{test.touchmove}}" bindtouchend="{{test.touchend}}" bindtouchstart="{{test.touchstart}}" class="page-top" style="color:white;">
<image bindtap="tap_ch" src="../../../images/btn.png"></image>
右滑出现侧边菜单
</view>
</view>
在 WXML 文件中,我们定义了一个 page
视图,包括一个 page-top
视图作为侧边菜单,和一个 page-bottom
视图包含内容。page-top
视图绑定了 bindtouchstart
、bindtouchmove
和 bindtouchend
事件,用于处理侧边菜单的滑动效果。通过 data-width
传递了窗口宽度,供 WXS 函数使用。
// sidebar.wxs
var newmark = startmark = 0
var status = 0
function touchstart(e, ins) {
var pageX = (e.touches[0] || e.changedTouches[0]).pageX
startmark = newmark = pageX
}
function touchmove(e, ins) {
var pageX = (e.touches[0] || e.changedTouches[0]).pageX
newmark = pageX
var data = {
windowWidth: e.target.dataset.width
}
if (startmark < pageX) {
if (data.windowWidth * 0.75 > Math.abs(newmark - startmark)) {
ins.selectComponent('.page-top').setStyle({
transform: 'translateX(' + Math.min(data.windowWidth * 0.75, ((status == 1 ? data.windowWidth * 0.75 : 0) + newmark - startmark)) + 'px)'
})
}
}
if (startmark > pageX) {
ins.selectComponent('.page-top').setStyle({
transform: 'translateX(' + Math.max(0, ((status == 1 ? data.windowWidth * 0.75 : 0) + newmark - startmark)) + 'px)'
})
}
}
function touchend(e, ins) {
var pageX = (e.touches[0] || e.changedTouches[0]).pageX
newmark = pageX
var data = {
windowWidth: e.target.dataset.width
}
if (startmark < pageX) {
if (data.windowWidth * 0.2 < Math.abs(newmark - startmark)) {
ins.selectComponent('.page-top').setStyle({
transform: 'translateX(' + (data.windowWidth * 0.75) + 'px)'
})
status = 1 // 展开状态
} else {
ins.selectComponent('.page-top').setStyle({
transform: 'translateX(0px)'
})
status = 0 // 收起状态
}
}
if (startmark > newmark) {
ins.selectComponent('.page-top').setStyle({
transform: 'translateX(0px)'
})
status = 0 // 收起状态
}
}
module.exports = {
touchstart: touchstart,
touchmove: touchmove,
touchend: touchend
}
在 WXS 文件中,定义了三个函数来处理触摸事件:touchstart
、touchmove
和 touchend
。
touchstart
:记录触摸开始时的横坐标。touchmove
:根据触摸的移动计算新的位置,并实时更新侧边菜单的位置。侧边菜单的最大滑动距离为窗口宽度的 75%。如果向右滑动,菜单逐渐显示;如果向左滑动,菜单逐渐隐藏。touchend
:在触摸结束时,根据滑动的距离决定侧边菜单的最终状态。如果滑动距离大于窗口宽度的 20%,菜单展开;否则,菜单收起。
基于滚动的动态fixed tabbar
<wxs module="test" src="./test.wxs"></wxs>
<scroll-view bindscroll="{{test.funcA}}" style='height:{{height}}px;' scroll-y>
<view class="page-banner">
<image class="image" src="/images/1.jpeg" style='width:100%;'></image>
</view>
<view class="page-group-interaction page-group" style='background-color:rgba(00, 00, 00, 0);'>
<view class="page-nav-list"><text>首页</text></view>
<view class="page-nav-list"><text>活动</text></view>
<view class="page-nav-list"><text>菜单</text></view>
<view class="page-nav-list"><text>我的</text></view>
</view>
<view class="goods-list">
</view>
<view class="goods-list">
goods-list
</view>
<view class="goods-list">
goods-list
</view>
<view class="goods-list">
goods-list
</view>
<view class="goods-list">
goods-list
</view>
</scroll-view>
在 WXML 文件中,scroll-view
组件用于创建一个可滚动的区域。绑定了 bindscroll
事件到 WXS 函数 funcA
,以便在滚动时触发样式更新。页面包含一个 page-banner
显示图片,一个 page-group
作为导航栏,以及多个 goods-list
视图用于显示商品。
// test.wxs
var funcA = function (e, ins) {
var scrollTop = e.detail.scrollTop
if (scrollTop > 100) {
ins.selectComponent('.page-group').setStyle({
"background-color": 'black'
}).addClass('page-group-position')
ins.selectComponent('.page-banner .image').setStyle({
opacity: 0
})
} else {
ins.selectComponent('.page-group').setStyle({
"background-color": 'rgba(00, 00, 00, ' + Math.max(0, (scrollTop) / 100) + ')'
}).removeClass('page-group-position')
ins.selectComponent('.page-banner .image').setStyle({
opacity: 1 - Math.max(0, (scrollTop) / 100)
})
}
}
module.exports = {
funcA: funcA
}
在 WXS 文件中,funcA
函数处理 scroll-view
的滚动事件:
- 滚动处理:根据滚动距离(
scrollTop
),动态调整page-group
和page-banner
中的元素样式。- 当
scrollTop
大于 100 时:page-group
背景色变为黑色,并添加page-group-position
类名。page-banner
中的图片透明度设置为 0,使其完全不可见。
- 当
scrollTop
小于等于 100 时:page-group
的背景色逐渐变为黑色,根据滚动距离调整透明度。page-banner
的图片透明度逐渐恢复,依据滚动距离调整。
- 当
自适应高度的 Swiper
<wxs module="test" src="./nearby.wxs"></wxs>
<swiper class="swiper" data-width="{{windowWidth}}" data-imgsize="{{imgSize}}" bindchange='{{test.change}}' bindanimationfinish="{{test.animationFinish}}" bindtransition="{{test.func}}" indicator-dots="{{indicatorDots}}"
autoplay="{{true}}" interval="{{interval}}" circular='' duration="{{duration}}">
<block wx:for="{{imgUrls}}">
<swiper-item style="height:100%;">
<image src="{{item}}" class="slide-image" style="height:100%;width:100%;" mode="center" height="300"/>
</swiper-item>
</block>
</swiper>
在 WXML 文件中,定义了一个 swiper
组件来显示一系列的图片。swiper
绑定了 change
、animationfinish
和 transition
事件,用于处理图片滑动时的逻辑。通过 data-width
和 data-imgsize
传递了窗口宽度和图片尺寸信息给 WXS 脚本。
// nearby.wxs
var func = function (e, instance) {
var dataset = e.instance.getDataset()
var st = e.instance.getState()
var current = st.current || 0
var imgsize = dataset.imgsize
var width = dataset.width
var detail = e.detail
var dx = e.detail.dx
var diff = typeof st.lastx !== 'undefined' ? (dx - st.lastx) : (dx - 0)
if (diff === 0) return
st.continueCount = st.continueCount || 1
if (Math.abs(dx) > width * st.continueCount && st.tmpcurrent != -1) {
current = st.tmpcurrent
st.current = st.tmpcurrent
st.tmpcurrent = -1
st.continueCount++
}
var setToWidth = false
var dir = dx > 0
if (dx !== 0 && Math.abs(dx) >= width) {
setToWidth = true
}
dx = dx - width * parseInt(dx / width)
if (dx === 0 && setToWidth) {
dx = dir ? width : -width
}
if (current >= imgsize.length - 1 && dx > 0) return
if (current <= 0 && dx < 0) return
var currentSize = imgsize[current]
var nextSize = dx > 0 ? imgsize[current + 1] : imgsize[current - 1]
var currentHeight = st.currentHeight || currentSize.height
var diffHeight = Math.abs((nextSize.height - currentSize.height) * (dx / width))
var realheight = currentSize.height + (nextSize.height - currentSize.height > 0 ? diffHeight : -diffHeight)
st.currentHeight = realheight
e.instance.setStyle({
height: realheight + 'px'
})
st.lastdir = dx > 0
}
module.exports = {
func: func,
change: function(e, instance) {
var st = e.instance.getState()
st.tmpcurrent = e.detail.current
},
animationFinish: function(e) {
var st = e.instance.getState()
if (typeof st.tmpcurrent === 'undefined' || st.tmpcurrent === -1) return
st.current = st.tmpcurrent
st.tmpcurrent = -1
st.continueCount = 1
}
}
在 WXS 文件中,我们定义了三个函数来处理 swiper
组件的滑动行为:
-
func
:处理滑动过程中的动态高度调整。根据用户的滑动距离(dx
),计算当前和下一个滑块的高度差,并更新swiper
组件的高度。确保在滑动过程中,滑块的高度平滑过渡,以适应内容的变化。 -
change
:在滑块切换时,更新状态中的临时当前索引(tmpcurrent
),用于在滑动过程中校正当前滑块的高度。 -
animationFinish
:在滑动动画结束后,更新实际的当前索引(current
),确保状态同步,并重置tmpcurrent
和continueCount
。
总结
WXS 函数通过将事件处理移至视图层,有效减少了层间通信的开销,提升了小程序的交互响应速度。使用 WXS 函数可以大大改善用户体验,特别是在需要高频次用户交互的场景中。