为了账号安全,请及时绑定邮箱和手机立即绑定
慕课专栏

目录

索引目录

mpvue原理深入解析36讲

原价 ¥ 49.00

立即订阅
08 第五式:事件处理器(上)
更新时间:2019-09-04 14:13:37
加紧学习,抓住中心,宁精勿杂,宁专勿多。

—— 周恩来

上一节我们学习了微信小程序和 mpvue 框架的条件渲染和列表渲染,这两块内容都是基础语法中比较重要的用法。

接下来我们将学习事件处理器,这部分内容我们也将从以下两方面进行学习:

  • 微信小程序的事件处理器
  • mpvue 框架的事件处理器

由于这部分内容比较多,所以我们将拆分成两个小节来进行学习。在本小节中,我们主要来学习微信小程序的事件处理器。

微信小程序事件处理器

事件是小程序开发的难点之一,其中概念多、逻辑复杂,本节将深入微信小程序的事件处理机制,并配合案例进行演示,最后还将介绍小程序 2.4.4 版本引入的新特性 wxs 事件绑定,以解决频繁交互场景下渲染效率问题。

事件捕获与冒泡

事件充斥在我们小程序之中,比如:用户点击按钮、两指缩放地图、使用指纹解锁付款等等,我们先从一个最简单的按钮点击引入事件:

# DOM 定义
<button bindtap="onTap">按钮</button>

# 事件定义
onTap(e) {
	console.log(e)
}

上面的案例中我们使用 bindtap 这个属性绑定了事件,它是 bind:tap 的简写,如同 mpvue 中我们使用 @click 简化 v-on:click 一样。我们点击按钮,可以在控制台中看到打印出一行日志:

{type: "tap", timeStamp: 1738, target: {}, currentTarget: {}, mark: {}, …}

这说明我们的事件触发成功。这里的 bindtap 绑定了冒泡事件,这个概念源于浏览器的事件绑定。我们可以使用 capture-bind:tap 绑定捕获事件。在单个按钮事件处理中,这两个事件几乎一致。微信小程序的事件处理分为两大阶段:捕获阶段冒泡阶段

  • 捕获阶段:事件自上而下依次传递;
  • 冒泡阶段:事件自下而上依次传递。

我们一起完成一个更复杂的案例,首先打开 my-wx-project 项目,修改 index.wxml 文件的内容:

<view 
  id="wrapper"
  bind:tap="bind1"
  capture-bind:tap="cbind1"
  style="background:red;color:white;text-align:center;padding:10px;"
>
  <view
    id="inner"
    bind:tap="bind2"
    capture-bind:tap="cbind2"
  >
    事件的捕获和冒泡测试
  </view>
</view>

再修改 index.js 文件的内容,添加四个事件:

Page({
  bind1(e) {
    console.log('wrapper冒泡')
  },
  bind2(e) {
    console.log('inner冒泡')
  },
  cbind1(e) {
    console.log('wrapper捕获')
  },
  cbind2(e) {
    console.log('inner捕获')
  }
})

这里定义了两个 view,两者互相嵌套关系,外层 id 为 wrapper,内层 id 为 inner。点击 inner 后,wrapper 获得捕获事件,然后传递给 inner 的捕获事件,至此捕获事件执行完毕。开始执行冒泡事件,首先执行 inner 的冒泡事件,然后执行 wrapper 的冒泡事件,此时控制台会依次打印如下日志:

wrapper捕获
inner捕获
inner冒泡
wrapper冒泡

上述捕获和冒泡流程图如下:

图片描述

事件拦截

在事件捕获和冒泡过程中一个常见的需求就是拦截某一个事件,使其执行完毕后不再向下/上传递,微信小程序给我们提供了 catch 事件来解决这类问题:

  • 捕获:使用 capture-catch 实现捕获拦截;
  • 冒泡:使用 catch 实现冒泡拦截。

下面举例说明,我们修改上一节的 index.wxml 的源码:

<view 
  id="wrapper"
  bindtap="bind1"
  capture-catch:tap="cbind1"
  style="background:red;color:white;text-align:center;padding:10px;"
>

通过 capture-catch 绑定 wrapper 的捕获事件,点击 inner 后的运行结果如下:

wrapper捕获

这是由于 wrapper 的捕获事件是第一个下发的,如果使用 capture-catch 绑定事件,将拦截该捕获事件后续的所有事件,包括冒泡。冒泡事件拦截与其类似,我们修改 inner 的源码:

<view
	id="inner"
	catchtap="bind2"
	capture-bind:tap="cbind2"
>

bindtap 改为 catchtap,此时点击 inner 后的运行结果如下:

wrapper捕获
inner捕获
inner冒泡

可以看到 inner 后的冒泡事件被拦截。

事件对象

微信小程序事件响应后,会向绑定事件中传递 event 参数,请看这样一个案例:

<view 
  id="btn-wrapper"
  capture-bind:tap="onTap"
  data-wrapper="wrapper"
  mark:wrapperMark="wrapperMark"
>
  <button 
    id="btn"
    data-btn="btn"
    mark:btnMark="btnMark"
  >
    按钮
  </button>
</view>

button 外包裹了一个 view,事件绑定到 view 上,viewbutton 上添加了 datamark 属性,这些后续小节会详细介绍,我们尝试点击按钮,然后打印事件中传递的 event 参数,如下图:

图片描述
该对象各属性的含义如下:

  • changedTouches:触摸发生变化时,触摸点信息构成的数组;
  • currentTarget:触发事件的组件;
  • detail:额外的信息;
  • mark:额外传递的事件标记数据;
  • target:实际响应事件的组件,与 Web 开发的概念非常类似;
  • timeStamp:页面打开到事件触发经过的毫秒数;
  • touches:事件触发后,停留在屏幕上的触摸点数组信息;
  • type:触发的事件类型。

touches 和 changedTouches 的主要区别是:touches 表示事件触发后,停留在屏幕上的触摸点信息,比如触摸事件 bindtouchend 触发时手指已经离开屏幕(如果手指不离开屏幕不会触发该事件),changedTouches 的值为触发事件时的触摸点信息(可以理解为达到最终状态前变化的触摸点信息,所以 changedTouches 不会为空),而 touches 则为空数组,因为事件触发后手指已经离开屏幕,无法获得到触摸点。

事件参数传递

微信小程序无法像 Vue 和 mpvue 一样直接向事件传递参数,我们必须通过定义 datamark 属性来传递参数。

参数定义

上面的案例中,参数定义关系如下:

  • wrapper 定义了 data-wrapper="wrapper"mark:wrapperMark="wrapperMark" 两个参数;
  • inner 定义了 data-btn="btn"mark:btnMark="btnMark" 两个参数。

这里的参数主要分为 datamark 两种,data 参数定义方法为添加 data- 属性,mark 参数定义方法为添加 mark: 属性,两者的区别是:

  • data 参数需要从 targetcurrentTarget 对象的 dataset 属性获取。由于 targetcurrentTarget 只代表一个组件,所以每个组件的参数需要从各自对应的 target 获取
  • 为了解决上述问题,微信小程序增加了 mark 参数。mark参数直接从 event 参数获取,它会将事件冒泡路径上所有的 mark 参数收集后合并返回。

关于 mark 参数有两个细节逻辑:
第一,如果事件冒泡时被拦截 mark 参数会合并吗?会合并;
第二,如果事件捕获时被拦截 mark 参数会合并吗?会合并。
也就是说只要触发了事件,那么最终响应的事件的 target 冒泡路径上的所有 DOMmark 参数都将被收集,不管它们的事件是否被触发。

参数获取

我们定义的参数会封装到 event 参数中传给我们的事件,获取方法如下:

  • data 参数:从 targetcurrentTarget 对象的 dataset 属性获取,上面的案例中,wrapper 的参数从 currentTarget 获取,currentTarget 的数据结构如下:
currentTarget: {
	dataset: {wrapper: "wrapper"}
	id: "btn-wrapper"
	offsetLeft: 0
	offsetTop: 88
}

inner 的参数从 target 获取,target 的数据结构如下:

target: {
	dataset: {btn: "btn"}
	id: "btn"
	offsetLeft: 0
	offsetTop: 88
}
  • mark 参数:从 event.mark 属性获取,mark 属性的数据结构如下:
mark: {
	btnMark: "btnMark"
	wrapperMark: "wrapperMark"
}

微信小程序事件绑定进阶

本节将通过一个可拖拽的 view 案例向大家介绍 touch 事件,并引入 wxs 事件绑定,通过该特性大幅提升渲染性能。

touch 事件

常用的 touch 事件共有三种,与 h5 开发非常类似:

  • touchstart:手指接触屏幕后,触发该事件,只会触发一次;
  • touchmove:手指按住屏幕,滑动手指时触发该事件,滑动过程中该事件会持续触发;
  • touchend:松开手指后,触摸行为停止,触发该事件,只会触发一次。

接下来我们通过状态绑定的方法实现一个可拖拽的 view 案例,打开 my-wx-project 项目,修改 index.wxml 文件的内容:

<view
  class="box"
  style="position:absolute;width:150px;height:150px;background:yellowGreen;left:{{left}}px;top:{{top}}px"
  bindtouchstart="onTouchStart"
  bindtouchmove="onTouchMove"
></view>

这个的布局文件非常简单,它只定义了一个 view,主要做了以下两件事:

  • 定义了 view 的基本样式,布局为绝对定位,拖拽时通过改变 topleft 来实现,所以在样式中绑定了 topleft 两个状态;
  • 绑定了 touchstart 和 touchmove 事件

我们再修改 index.js 文件的内容:

let startX = 0, startY = 0 // 记录触摸点的x,y轴坐标
Page({
  data: {
    top: 100, // 定义 view 到屏幕顶部的距离
    left: 100 // 定义 view 到屏幕左侧的距离
  },
  onTouchStart(event) {
    const [{pageX, pageY}] = event.touches || event.changedTouches // 初次接触屏幕时,记录触摸点的x,y轴坐标
    startX = pageX
    startY = pageY
  },
  onTouchMove(event) {
    const [{pageX, pageY}] = event.touches || event.changedTouches
    const offsetX = pageX - startX // 计算触摸点 x 轴的偏移量,pageX为当前触摸点为止,startX为上次触摸点的位置,两者相减获得当前的偏移量
    const offsetY = pageY - startY // 计算触摸点 y 轴的偏移量
    startX = pageX // 将当前触摸点的 x 坐标作为下次的起始位置
    startY = pageY // 将当前触摸点的 y 坐标作为下次的起始位置
    this.setData({
      left: offsetX + this.data.left, // 用当前位置加上偏移量得到最新的位置
      top: offsetY + this.data.top // 同上
    })
  }
})

上述代码运行后的效果如下:

图片描述
这里我们虽然实现了触摸功能,但仔细观察会发现拖动时有一些卡顿,这是因为我们通过频繁调用 setData 修改 topleft 的状态实现,setData 造成卡顿的主要原因是由于微信小程序的视图层(Webview)和逻辑层(App Service)是两个线程,两者间通信的成本较高,所以不建议大家频繁调用 setData,为了解决这个问题,微信小程序引入 wxs 事件绑定机制。

关于 setData 的更多使用技巧,大家可以查看官方文档

wxs 事件绑定

wxx 事件绑定是微信小程序 2.4.4 版本新引入的机制,通过 wxs 我们可以对上述场景进行改造,通过直接更新 style 的方式减少 setData 的调用。下面我们对上述场景进行移植,首先修改 index.wxs 文件的内容:

var startX = 0, startY = 0, offsetX = 100, offsetY = 100

function onTouchStart(event, instance) {
  var touch = event.touches[0] || event.changedTouches[0]
  startX = touch.pageX
  startY = touch.pageY
  return false // 阻止冒泡
}

function onTouchMove(event, instance) {
  var touch = event.touches[0] || event.changedTouches[0]
  var pageX = touch.pageX
  var pageY = touch.pageY
  offsetX = pageX - startX + offsetX
  offsetY = pageY - startY + offsetY
  startX = pageX
  startY = pageY
  event.instance.setStyle({
    left: offsetX + 'px',
    top: offsetY + 'px'
  }) // 动态修改 view 的style
  return false // 阻止冒泡
}

module.exports = {
  onTouchStart: onTouchStart,
  onTouchMove: onTouchMove
}

以上完成了 wxs 的定义,下面需要在 index.wxml 中进行调用:

<wxs module="utils" src="./index.wxs" />
<view
  class="box" 
  style="position:absolute;width:150px;height:150px;background:yellowGreen;left:100px;top:100px"
  bindtouchstart="{{utils.onTouchStart}}"
  bindtouchmove="{{utils.onTouchMove}}"
></view>

运行后拖拽 view 可以发现流畅度明显提高了很多:

图片描述

gif 动画中不太明显,大家可以到真机或模拟器中进行测试

wxs 的事件绑定与普通的事件绑定整体实现方式差别不大,但其中有两处重要区别:

  • 事件入参不同:普通事件绑定入参只有 event,而 wxs 增加了 instance 参数,同时 event 中也增加了 instance 参数,我们可以查看 event 的返回结果:
{
    "type":"touchstart",
    "timeStamp":1257,
    "target":{
        "id":"box",
        "offsetLeft":100,
        "offsetTop":100,
        "dataset":{

        }
    },
    "currentTarget":{
        "id":"box",
        "offsetLeft":100,
        "offsetTop":100,
        "dataset":{

        }
    },
    "mark":{

    },
    "touches":[
        {
            "identifier":0,
            "pageX":200,
            "pageY":193,
            "clientX":200,
            "clientY":193,
            "force":1
        }
    ],
    "changedTouches":[
        {
            "identifier":0,
            "pageX":200,
            "pageY":193,
            "clientX":200,
            "clientY":193,
            "force":1
        }
    ],
    "instance":{
        "selectAllComponents":null,
        "selectComponent":null,
        "removeClass":null,
        "addClass":null,
        "hasClass":null,
        "setStyle":null,
        "getDataset":null,
        "getState":null,
        "triggerEvent":null,
        "callMethod":null,
        "requestAnimationFrame":null
    }
}

这里的 instance 是一个 ComponentDescriptor 实例,它提供了一系列工具方法,包括修改 styleclass 、状态等,event.instanceinstance 参数的区别是 event.instance 代表当前事件触发组件的 ComponentDescriptor,而 instance 代表 Page 级别的 ComponentDescriptor。上述 wxs 案例实现界面更新的核心代码是:

event.instance.setStyle({
  left: offsetX + 'px',
  top: offsetY + 'px'
})

这里我们直接调用了 event.instancesetStyle 方法更新样式,这里的 event.instance 对应我们拖动 viewComponentDescriptor,我们还可以通过 instance 实现完全一样的效果:

instance.selectComponent('.box').setStyle({
	left: offsetX + 'px',
	top: offsetY + 'px'
})

这里的 instance.selectComponent('.box') 做了一个组件查询的操作,它查询界面上 classbox 的组件,并返回该组件的 ComponentDescriptor ,然后我们再调用 setStyle 完成样式更新。

大家要注意,下面的代码将无法正常工作:

event.instance.selectComponent('.box').setStyle({
	left: offsetX + 'px',
	top: offsetY + 'px'
})

这是因为 event.instance.selectComponent 只能向下查找组件,这里的 view 下属并没有 classbox 的组件,所以返回结果为 null

除此之外还有几下点需要注意:

  • wxs 事件绑定时必须使用 {{}},而普通的事件绑定时不需要;
  • wxs 不支持 es6 语法,所以之前各种 es6 语法(数组+对象解构、let、const 等)在 wxs 中用不了;
  • wxs 通过函数值返回值控制冒泡,返回 false 时可以拦截冒泡;
  • wxs 通过 instance.callMethod 调用 App Service 中的方法(本例中指的是调用 index.js 中的方法),具体方法如下:
instance.callMethod('test', { a: 1 }) // 第一个参数为函数名,第二个参数是调用函数时传入的参数

instance 更多用法可以查看官方文档

  • wxs 目前只支持默认组件,不支持自定义组件。
}
立即订阅 ¥ 49.00

你正在阅读课程试读内容,订阅后解锁课程全部内容

千学不如一看,千看不如一练

手机
阅读

扫一扫 手机阅读

mpvue原理深入解析36讲
立即订阅 ¥ 49.00

举报

0/150
提交
取消