用WXS优化小程序事件响应

244 阅读6分钟

背景

在小程序开发中,用户交互效果如页面元素拖动可能会因频繁的事件通信导致卡顿。事件处理流程通常如下:

  1. 事件传递touchmove 事件从视图层(Webview)传递到逻辑层(App Service)。
  2. 事件处理:逻辑层处理事件并通过 setData 更新组件位置。
  3. 渲染:更新触发渲染,并可能导致脚本执行阻塞,影响用户体验。

这种方式的主要问题在于频繁的层间通信和渲染操作,导致动画和交互过程的延迟。

实现方案

为了减少通信次数并提升响应速度,可以将事件处理移至视图层(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

  1. 在 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 事件。

  2. 在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 处理触摸移动事件,propObserverprop 属性变化时触发。

场景应用

移动视图

139_1724919516-ezgif.com-crop.gif

<!-- 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 视图。通过 bindtouchstartbindtouchmovebindtouchend 绑定了 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 文件中,定义了两个函数:touchstarttouchmovetouchstart 函数记录了触摸的起始位置,并通过 callMethod 调用了逻辑层的 testCallmethod 方法。touchmove 函数根据触摸的移动计算视图的新位置,并使用 selectComponentsetStyle 方法更新视图的位置。通过这些操作,视图能够实时响应拖动手势,实现流畅的用户交互体验。

侧边菜单滑动

ezgif.com-crop.gif

<!-- 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 视图绑定了 bindtouchstartbindtouchmovebindtouchend 事件,用于处理侧边菜单的滑动效果。通过 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 文件中,定义了三个函数来处理触摸事件:touchstarttouchmovetouchend

  • touchstart:记录触摸开始时的横坐标。
  • touchmove:根据触摸的移动计算新的位置,并实时更新侧边菜单的位置。侧边菜单的最大滑动距离为窗口宽度的 75%。如果向右滑动,菜单逐渐显示;如果向左滑动,菜单逐渐隐藏。
  • touchend:在触摸结束时,根据滑动的距离决定侧边菜单的最终状态。如果滑动距离大于窗口宽度的 20%,菜单展开;否则,菜单收起。

基于滚动的动态fixed tabbar

141_1724919520-ezgif.com-crop.gif

<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-grouppage-banner 中的元素样式。
    • scrollTop 大于 100 时:
      • page-group 背景色变为黑色,并添加 page-group-position 类名。
      • page-banner 中的图片透明度设置为 0,使其完全不可见。
    • scrollTop 小于等于 100 时:
      • page-group 的背景色逐渐变为黑色,根据滚动距离调整透明度。
      • page-banner 的图片透明度逐渐恢复,依据滚动距离调整。

自适应高度的 Swiper

ezgif.com-crop .gif

<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 绑定了 changeanimationfinishtransition 事件,用于处理图片滑动时的逻辑。通过 data-widthdata-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),确保状态同步,并重置 tmpcurrentcontinueCount

总结

WXS 函数通过将事件处理移至视图层,有效减少了层间通信的开销,提升了小程序的交互响应速度。使用 WXS 函数可以大大改善用户体验,特别是在需要高频次用户交互的场景中。