🌟事件详解
🌟事件分类
事件分为冒泡事件和非冒泡事件:
- 冒泡事件:当一个组件上的事件被触发后,该事件会向父节点传递。
- 非冒泡事件:当一个组件上的事件被触发后,该事件不会向父节点传递。
WXML的冒泡事件列表:
类型 | 触发条件 | 最低版本 |
touchstart | 手指触摸动作开始 | |
touchmove | 手指触摸后移动 |
touchcancel | 手指触摸动作被打断,如来电提醒,弹窗 | |
touchend | 手指触摸动作结束 | |
tap | 手指触摸后马上离开 |
longpress | 手指触摸后,超过350ms再离开,如果指定了事件回调函数并触发了这个事件,tap事件将不被触发 | 1.5.0 |
longtap | 手指触摸后,超过350ms再离开(推荐使用longpress事件代替) |
transitionend | 会在 WXSS transition 或 wx.createAnimation 动画结束后触发 | |
animationstart | 会在一个 WXSS animation 动画开始时触发 |
animationiteration | 会在一个 WXSS animation 一次迭代结束时触发 | |
animationend | 会在一个 WXSS animation 动画完成时触发 |
touchforcechange | 在支持 3D Touch 的 iPhone 设备,重按时会触发 | 1.9.90 |
注:除上表之外的其他组件自定义事件如无特殊声明都是非冒泡事件,如form 的submit事件,input 的input事件,scroll-view 的scroll事件,(详见各个组件)
🌟普通事件绑定
事件绑定的写法类似于组件的属性,如:
<view bindtap="handleTap"> Click here! </view>
如果用户点击这个 view ,则页面的 handleTap 会被调用。
事件绑定函数可以是一个数据绑定,如:
<view bindtap="{{ handlerName }}"> Click here! </view>
此时,页面的 this.data.handlerName 必须是一个字符串,指定事件处理函数名;如果它是个空字符串,则这个绑定会失效(可以利用这个特性来暂时禁用一些事件)。
自基础库版本 1.5.0 起,在大多数组件和自定义组件中, bind 后可以紧跟一个冒号,其含义不变,如 bind:tap 。基础库版本 2.8.1 起,在所有组件中开始提供这个支持。
🌟绑定并阻止事件冒泡
除 bind 外,也可以用 catch 来绑定事件。与 bind 不同, catch 会阻止事件向上冒泡。
例如在下边这个例子中,点击 inner view 会先后调用handleTap3和handleTap2(因为tap事件会冒泡到 middle view,而 middle view 阻止了 tap 事件冒泡,不再向父节点传递),点击 middle view 会触发handleTap2,点击 outer view 会触发handleTap1。
<view id="outer" bindtap="handleTap1"> outer view <view id="middle" catchtap="handleTap2"> middle view <view id="inner" bindtap="handleTap3"> inner view </view> </view> </view>
🌟互斥事件绑定
自基础库版本 2.8.2 起,除 bind 和 catch 外,还可以使用 mut-bind 来绑定事件。一个 mut-bind 触发后,如果事件冒泡到其他节点上,其他节点上的 mut-bind 绑定函数不会被触发,但 bind 绑定函数和 catch 绑定函数依旧会被触发。
换而言之,所有 mut-bind 是“互斥”的,只会有其中一个绑定函数被触发。同时,它完全不影响 bind 和 catch 的绑定效果。
例如在下边这个例子中,点击 inner view 会先后调用 handleTap3 和 handleTap2 ,点击 middle view 会调用 handleTap2 和 handleTap1 。
<view id="outer" mut-bind:tap="handleTap1"> outer view <view id="middle" bindtap="handleTap2"> middle view <view id="inner" mut-bind:tap="handleTap3"> inner view </view> </view> </view>
🌟事件的捕获阶段
自基础库版本 1.5.0 起,触摸类事件支持捕获阶段。捕获阶段位于冒泡阶段之前,且在捕获阶段中,事件到达节点的顺序与冒泡阶段恰好相反。需要在捕获阶段监听事件时,可以采用capture-bind、capture-catch关键字,后者将中断捕获阶段和取消冒泡阶段。
在下面的代码中,点击 inner view 会先后调用handleTap2、handleTap4、handleTap3、handleTap1。
<view id="outer" bind:touchstart="handleTap1" capture-bind:touchstart="handleTap2"> outer view <view id="inner" bind:touchstart="handleTap3" capture-bind:touchstart="handleTap4"> inner view </view> </view>
如果将上面代码中的第一个capture-bind改为capture-catch,将只触发handleTap2。
<view id="outer" bind:touchstart="handleTap1" capture-catch:touchstart="handleTap2"> outer view <view id="inner" bind:touchstart="handleTap3" capture-bind:touchstart="handleTap4"> inner view </view> </view>
🌟事件对象
如无特殊说明,当组件触发事件时,逻辑层绑定该事件的处理函数会收到一个事件对象。
🌟BaseEvent 基础事件对象属性列表:
属性 | 类型 | 说明 | 基础库版本 |
type | String | 事件类型 |
timeStamp | Integer | 事件生成时的时间戳 | |
target | Object | 触发事件的组件的一些属性值集合 | |
currentTarget | Object | 当前组件的一些属性值集合 |
mark | Object | 事件标记数据 | 2.7.1 |
🌟CustomEvent 自定义事件对象属性列表(继承 BaseEvent):
属性 | 类型 | 说明 |
detail | Object | 额外的信息 |
🌟TouchEvent触摸事件对象属性列表(继承 BaseEvent):
属性 | 类型 | 说明 |
touches | Array | 触摸事件,当前停留在屏幕中的触摸点信息的数组 |
changedTouches | Array | 触摸事件,当前变化的触摸点信息的数组 |
特殊事件: canvas 中的触摸事件不可冒泡,所以没有 currentTarget。 |
🌟type
代表事件的类型。
timeStamp
页面打开到触发事件所经过的毫秒数。
🌟target
触发事件的源组件。
属性 | 类型 | 说明 |
id | String | 事件源组件的id |
dataset | Object | 事件源组件上由data-开头的自定义属性组成的集合 |
🌟currentTarget
事件绑定的当前组件。
属性 | 类型 | 说明 |
id | String | 当前组件的id |
dataset | Object | 当前组件上由data-开头的自定义属性组成的集合 |
说明: target 和 currentTarget 可以参考上例中,点击 inner view 时,handleTap3 收到的事件对象 target 和 currentTarget 都是 inner,而 handleTap2 收到的事件对象 target 就是 inner,currentTarget 就是 middle。
🌟dataset
在组件节点中可以附加一些自定义数据。这样,在事件中可以获取这些自定义的节点数据,用于事件的逻辑处理。
在 WXML 中,这些自定义数据以 data- 开头,多个单词由连字符 - 连接。这种写法中,连字符写法会转换成驼峰写法,而大写字符会自动转成小写字符。如:
- data-element-type ,最终会呈现为 event.currentTarget.dataset.elementType ;
- data-elementType ,最终会呈现为 event.currentTarget.dataset.elementtype 。
示例:
<view data-alpha-beta="1" data-alphaBeta="2" bindtap="bindViewTap"> DataSet Test </view> Page({ bindViewTap:function(event){ event.currentTarget.dataset.alphaBeta === 1 // - 会转为驼峰写法 event.currentTarget.dataset.alphabeta === 2 // 大写会转为小写 } })
🌟mark
在基础库版本 2.7.1 以上,可以使用 mark 来识别具体触发事件的 target 节点。此外, mark 还可以用于承载一些自定义数据(类似于 dataset )。
当事件触发时,事件冒泡路径上所有的 mark 会被合并,并返回给事件回调函数。(即使事件不是冒泡事件,也会 mark 。)
代码示例:
<view mark:myMark="last" bindtap="bindViewTap"> <button mark:anotherMark="leaf" bindtap="bindButtonTap">按钮</button> </view>
在上述 WXML 中,如果按钮被点击,将触发 bindViewTap 和 bindButtonTap 两个事件,事件携带的 event.mark 将包含 myMark 和 anotherMark 两项。
Page({ bindViewTap: function(e) { e.mark.myMark === "last" // true e.mark.anotherMark === "leaf" // true } })
mark 和 dataset 很相似,主要区别在于: mark 会包含从触发事件的节点到根节点上所有的 mark: 属性值;而 dataset 仅包含一个节点的 data- 属性值。
细节注意事项:
- 如果存在同名的 mark ,父节点的 mark 会被子节点覆盖。
- 在自定义组件中接收事件时, mark 不包含自定义组件外的节点的 mark 。
- 不同于 dataset ,节点的 mark 不会做连字符和大小写转换。
🌟touches
touches 是一个数组,每个元素为一个 Touch 对象(canvas 触摸事件中携带的 touches 是 CanvasTouch 数组)。 表示当前停留在屏幕上的触摸点。
🌟Touch 对象
属性 | 类型 | 说明 |
identifier | Number | 触摸点的标识符 |
pageX, pageY | Number | 距离文档左上角的距离,文档的左上角为原点 ,横向为X轴,纵向为Y轴 |
clientX, clientY | Number | 距离页面可显示区域(屏幕除去导航条)左上角距离,横向为X轴,纵向为Y轴 |
🌟CanvasTouch 对象
属性 | 类型 | 说明 |
identifier | Number | 触摸点的标识符 |
x, y | Number | 距离 Canvas 左上角的距离,Canvas 的左上角为原点 ,横向为X轴,纵向为Y轴 |
🌟CanvasTouch changedTouches
changedTouches 数据格式同 touches。 表示有变化的触摸点,如从无变有(touchstart),位置变化(touchmove),从有变无(touchend、touchcancel)。
🌟detail
自定义事件所携带的数据,如表单组件的提交事件会携带用户的输入,媒体的错误事件会携带错误信息,详见组件定义中各个事件的定义。
点击事件的detail 带有的 x, y 同 pageX, pageY 代表距离文档左上角的距离。
🌟WXS响应事件
🌟背景
有频繁用户交互的效果在小程序上表现是比较卡顿的,例如页面有 2 个元素 A 和 B,用户在 A 上做 touchmove 手势,要求 B 也跟随移动,movable-view 就是一个典型的例子。一次 touchmove 事件的响应过程为:
- a、touchmove 事件从视图层(Webview)抛到逻辑层(App Service)
- b、逻辑层(App Service)处理 touchmove 事件,再通过 setData 来改变 B 的位置
一次 touchmove 的响应需要经过 2 次的逻辑层和渲染层的通信以及一次渲染,通信的耗时比较大。此外 setData 渲染也会阻塞其它脚本执行,导致了整个用户交互的动画过程会有延迟。
🌟实现方案
本方案基本的思路是减少通信的次数,让事件在视图层(Webview)响应。小程序的框架分为视图层(Webview)和逻辑层(App Service),这样分层的目的是管控,开发者的代码只能运行在逻辑层(App Service),而这个思路就必须要让开发者的代码运行在视图层(Webview),如下图所示的流程:
使用 WXS 函数用来响应小程序事件,目前只能响应内置组件的事件,不支持自定义组件事件。WXS 函数的除了纯逻辑的运算,还可以通过封装好的ComponentDescriptor 实例来访问以及设置组件的 class 和样式,对于交互动画,设置 style 和 class 足够了。WXS 函数的例子如下:
var wxsFunction = function(event, ownerInstance) { var instance = ownerInstance.selectComponent('.classSelector') // 返回组件的实例 instance.setStyle({ "font-size": "14px" // 支持rpx }) instance.getDataset() instance.setClass(className) // ... return false // 不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault }
其中入参 event 是小程序事件对象基础上多了 event.instance 来表示触发事件的组件的 ComponentDescriptor 实例。ownerInstance 表示的是触发事件的组件所在的组件的
ComponentDescriptor 实例,如果触发事件的组件是在页面内的,ownerInstance 表示的是页面实例。
ComponentDescriptor的定义如下:
方法 | 参数 | 描述 | 最低版本 |
selectComponent | selector对象 | 返回组件的 ComponentDescriptor 实例。 |
selectAllComponents | selector对象数组 | 返回组件的 ComponentDescriptor 实例数组。 | |
setStyle | Object/string | 设置组件样式,支持rpx。 | 设置的样式优先级比组件 wxml 里面定义的样式高。不能设置最顶层页面的样式。 |
addClass/removeClass/hasClass | string | 设置组件的 class。设置的 class 优先级比组件 wxml 里面定义的 class 高。不能设置最顶层页面的 class。 | |
getDataset | 无 | 返回当前组件/页面的 dataset 对象 |
callMethod | (funcName:string, args:object) | 调用当前组件/页面在逻辑层(App Service)定义的函数。funcName表示函数名称,args表示函数的参数。 |
requestAnimationFrame | Function | 和原生 requestAnimationFrame 一样。用于设置动画。 | |
getState | 无 | 返回一个object对象,当有局部变量需要存储起来后续使用的时候用这个方法。 |
triggerEvent | (eventName, detail) | 和组件的triggerEvent一致。 | |
getComputedStyle | Array.<string> |
参数与 SelectorQuery 的 computedStyle 一致。 | 2.11.2 |
setTimeout | (Function, Number) | 与原生 setTimeout 一致。用于创建定时器。 | 2.14.2 |
clearTimeout | Number | 与原生 clearTimeout 一致。用于清除定时器。 | 2.14.2 |
getBoundingClientRect | 无 | 返回值与 SelectorQuery 的 boundingClientRect 一致。 | 2.14.2 |
WXS 运行在视图层(Webview),里面的逻辑毕竟能做的事件比较少,需要有一个机制和逻辑层(App Service)开发者的代码通信,上面的 callMethod 是 WXS 里面调用逻辑层(App Service)开发者的代码的方法,而 WxsPropObserver 是逻辑层(App Service)开发者的代码调用 WXS 逻辑的机制。
🌟使用方法
- WXML定义事件:
<wxs module="test" src="./test.wxs"></wxs> <view change:prop="{{test.propObserver}}" prop="{{propValue}}" bindtouchmove="{{test.touchmove}}" class="movable"></view>
上面的change:prop(属性前面带change:前缀)是在 prop 属性被设置的时候触发 WXS 函数,值必须用{{}}括起来。类似 Component 定义的 properties 里面的 observer 属性,在setData({propValue: newValue})调用之后会触发。
注意:WXS函数必须用{{}}括起来。当 prop 的值被设置 WXS 函数就会触发,而不只是值发生改变,所以在页面初始化的时候会调用一次WxsPropObserver的函数。
WXS文件test.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) } }
- 目前还不支持原生组件的事件、input和textarea组件的 bindinput 事件
- 1.02.1901170及以后版本的开发者工具上支持交互动画,最低版本基础库是2.4.4
- 目前在WXS函数里面仅支持console.log方式打日志定位问题,注意连续的重复日志会被过滤掉。
🌟简易双向绑定
基础库 2.9.3 开始支持,低版本需做兼容处理。
🌟双向绑定语法
在 WXML 中,普通的属性的绑定是单向的。例如:
<input value="{{value}}" />
如果使用 this.setData({ value: ‘leaf’ }) 来更新 value ,this.data.value 和输入框的中显示的值都会被更新为 leaf ;但如果用户修改了输入框里的值,却不会同时改变 this.data.value 。
如果需要在用户输入的同时改变 this.data.value ,需要借助简易双向绑定机制。此时,可以在对应项目之前加入 model: 前缀:
<input model:value="{{value}}" />
这样,如果输入框的值被改变了, this.data.value 也会同时改变。同时, WXML 中所有绑定了 value 的位置也会被一同更新, 数据监听器 也会被正常触发。
用于双向绑定的表达式有如下限制:
- 只能是一个单一字段的绑定,如
<input model:value="值为 {{value}}" /> <input model:value="{{ a + b }}" />
都是非法的;
- 目前,尚不能 data 路径,如
<input model:value="{{ a.b }}" />
这样的表达式目前暂不支持。
🌟在自定义组件中传递双向绑定
双向绑定同样可以使用在自定义组件上。如下的自定义组件:
// custom-component.js Component({ properties: { myValue: String } })
<!-- custom-component.wxml --> <input model:value="{{myValue}}" />
这个自定义组件将自身的 myValue 属性双向绑定到了组件内输入框的 value 属性上。这样,如果页面这样使用这个组件:
<custom-component model:my-value="{{pageValue}}" />
当输入框的值变更时,自定义组件的 myValue 属性会同时变更,这样,页面的 this.data.pageValue 也会同时变更,页面 WXML 中所有绑定了 pageValue 的位置也会被一同更新。
🌟在自定义组件中触发双向绑定更新
自定义组件还可以自己触发双向绑定更新,做法就是:使用 setData 设置自身的属性。例如:
// custom-component.js Component({ properties: { myValue: String }, methods: { update: function() { // 更新 myValue this.setData({ myValue: 'leaf' }) } } })
如果页面这样使用这个组件:
<custom-component model:my-value="{{pageValue}}" />
当组件使用 setData 更新 myValue 时,页面的 this.data.pageValue 也会同时变更,页面 WXML 中所有绑定了 pageValue 的位置也会被一同更新。
🌟基础组件
框架为开发者提供了一系列基础组件,开发者可以通过组合这些基础组件进行快速开发。详细介绍请参考组件文档。
🌟什么是组件:
- 组件是视图层的基本组成单元。
- 组件自带一些功能与微信风格一致的样式。
- 一个组件通常包括 开始标签 和 结束标签,属性 用来修饰这个组件,内容 在两个标签之内。
<tagname property="value"> Content goes here ... </tagname>
注意:所有组件与属性都是小写,以连字符-连接
🌟属性类型
类型 | 描述 | 注解 |
Boolean | 布尔值 | 组件写上该属性,不管是什么值都被当作 true;只有组件上没有该属性时,属性值才为false。如果属性值为变量,变量的值会被转换为Boolean类型 |
Number | 数字 | 1, 2.5 |
String | 字符串 | “string” |
Array | 数组 | [ 1, “string” ] |
Object | 对象 | { key: value } |
EventHandler | 事件处理函数名 | "handlerName" 是 Page 中定义的事件处理函数名 |
Any | 任意属性 |
🌟公共属性
所有组件都有以下属性:
属性名 | 类型 | 描述 | 注解 |
id | String | 组件的唯一标示 | 保持整个页面唯一 |
class | String | 组件的样式类 | 在对应的 WXSS 中定义的样式类 |
style | String | 组件的内联样式 | 可以动态设置的内联样式 |
hidden | Boolean | 组件是否显示 | 所有组件默认显示 |
data-* | Any | 自定义属性 | 组件上触发的事件时,会发送给事件处理函数 |
bind* / catch* | EventHandler | 组件的事件 | 详见事件 |
🌟特殊属性
几乎所有组件都有各自定义的属性,可以对该组件的功能或样式进行修饰,请参考各个组件的定义。
🌟结语
这篇为小伙伴们讲解小程序的架构之【视图层】的知识;水滴石穿,积少成多。各位小伙伴让我们 let’s be prepared at all times!