TOC

事件处理

    小程序内的事件处理和JavaScript内的事件处理基本上没有什么太大的差异的,只不过有一些微小的变化,在小程序内,是经常需要和用户进行某种交互的,比如点击街面上某个按钮,或者某个区域,或者滑动了某个区域,那么这个时候,如果我们希望捕获到这些事件,就必须采取事件监听的方式来获取;
    事件可以绑定在组件上,当事件发生时,就会支持逻辑层中对应的事件处理函数,同时,事件发生时,默认会给事件处理函数传递一个事件对象,在这个事件对象内,默认会携带一些信息;
    事件通过bind/catch属性来将事件绑定在组件上,和普通的属性写法上一致的,以key/value的形式,从基础库1.5.0版本开始,可以在bind和catch后面加上一个冒号,同时,在当前页面Page构造器定义对应的事件处理函数,当用户触发了指定的事件,就会立即调用对应的事件处理函数进行相应的处理,并在在调用时,默认会传递一个事件对象给事件处理函数,所以每一个事件处理函数默认都可以接受一个事件对象的参数;

事件类型

    对于事件类型来讲,其实在此之前学习过的一些组件,默认就有有一些自有的事件,比如input有bindinput、bindblur等事件,比如scroll-view有bindscrolltowpper、bindscrolltolower等事件,这些组件自身的事件属于组件专有事件,具体的还请查看官方文档;
    那么在此处主要讨论公共事件,在小程序内,默认有七个公共事件,如下列表;
事件类型
触发条件
touchstart
手指触摸动作开始,即点击不松开
touchmove
手指触摸后移动,即点击不松开拖动
touchcancel
手指触摸被打断,如来电提醒,弹窗等
touchend
手指触摸动作结束,即松开
tap
手指触摸后马上离开,即点击
longpress
手指触摸后,超过350ms再离开,如果指定了事件回调函数并触发该事件,tab事件将不被触发
longtap
手指触摸后,超过350ms再离开(推荐使用longpress)

事件对象

    当某个事件触发时会产生一个事件对象,然后会直接将这个事件对象传递给回调函数,然后开发者,可以根据事件对象中的一些数据作出相应的处理,那么这个事件对象里面的属性其实是非常多的,常用如下;
type:字符串类型,表示此次事件的类型;
timeStamp:数字类型,表示事件生成时的时间戳;
traget:对象类型,表示触发事件的组件上的一些属性集合,可能存在冒泡,或者嵌套元素;
currentTarget:对象类型,当前组件的属性集合,可能存在冒泡,或者嵌套元素;
mark:对象类型,表示事件标记的数据;
    这里唯一需要注意一点,就是target和currentTarget,如果存在一个嵌套组件,我们在外层组件监听对应的事件,内层组件并未监听对应的事件,但是如果我们点击内层组件,一样会触发事,那么此时,target就是内层组件,而currentTarget就是外层组件,所以currentTarget代表,触发事件的组件,而target表示发生事件的组件,如下示例;
<!-- pages/page/page.wxml -->
<view id="1" class="outer" bind:tap="EventFunction">
    <view id="2" class="inner"></view>
</view>

/* pages/page/page.wxss */
.outer {
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: red;
    width: 300rpx;
    height: 300rpx;
    margin: 10px;
}

.inner {
    background-color: black;
    width: 100rpx;
    height: 100rpx;
}

// pages/page/page.js
Page({
    EventFunction(event) {
        console.log(event.target)
        console.log(event.currentTarget)
    }
})

    当点击内层组件时,结果如下;
{id: "2", offsetLeft: 43, offsetTop: 43, dataset: {…}}
{id: "1", offsetLeft: 0, offsetTop: 0, dataset: {…}}
  • 注意:所以,从结果来看,如果我们通过data-var_name给事件函数传参,那么推荐使用event.currentTarget来获取;

事件传参

    对于小程序当全局类型当事件传参,有点独特,它是通过组件属性的方式来传递的,即通过data-var_name的形式给事件处理函数传参的,data-为固定格式,后面的参数为标识符,但是需要注意,它并非直接将这个数据通过实参的形式传递给事件处理函数,而是将这个数据存放在event对象内,默认会存放在target或者currentTarget的dataset对象中,但是由于上面说的target和currentTarget的问题,所以建议直接从currentTarget中的dataset对象获取传递过来的数据;
<!-- pages/page/page.wxml -->
<view class="outer" bind:tap="EventFunction" data-number="{{1}}" />

// pages/page/page.js
Page({
    EventFunction(event) {
        console.log(event.target.dataset)
        console.log(event.currentTarget.dataset)
    }
})

# 结果
{number: 1}
{number: 1}

事件冒泡和捕获

    如果我们的组件之间存在嵌套关系,且嵌套间的组件都存在一个共同的事件,那么就可能会产生事件冒泡或者事件捕获,从外层向内层传递,称之为事件捕获,从内层向外层传递称之为事件冒泡;
事件捕获
    事件捕获就是,从外层向内层延伸,层层触发对应的事件,那么对于事件捕获来讲,它的事件类型有点特殊,如果我们希望实现事件捕获,那么在组件上定义事件属性时,需要以capture-开头,如点击事件,则为capture-bindtab,如下;
<!-- pages/page/page.wxml -->
<view class="view1" capture-bind:tap="view1CaptureEvent">
    <view class="view2" capture-bind:tap="view2CaptureEvent">
        <view class="view3" capture-bind:tap="view3CaptureEvent"></view>
    </view>
</view>

/* pages/page/page.wxss */
.view1 {
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: red;
    width: 600rpx;
    height: 600rpx;
}

.view2 {
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: blue;
    width: 400rpx;
    height: 400rpx;
}

.view3 {
    background-color: green;
    width: 200rpx;
    height: 200rpx;
}

// pages/page/page.js
Page({
    view1CaptureEvent() {
        console.log('view1CaptureEvent')
    },
    view2CaptureEvent() {
        console.log('view2CaptureEvent')
    },
    view3CaptureEvent() {
        console.log('view3CaptureEvent')
    }
})
    当我们点击最内层的组件时,就会触发事件捕获,从外层向内层层层触发点击时,直到被点击的组件为止,如下,点击最内层的结果;
view1CaptureEvent
view2CaptureEvent
view3CaptureEvent
事件冒泡
    事件冒泡就是,从内层向外层延伸,层层触发对应的事件,小程序在默认情况下就是事件冒泡的,所以,对于事件冒泡来讲,针对事件的定义,我们无需以capture-开头,如下;
<!-- pages/page/page.wxml -->
<view class="view1" bind:tap="view1CaptureEvent">
    <view class="view2" bind:tap="view2CaptureEvent">
        <view class="view3" bind:tap="view3CaptureEvent"></view>
    </view>
</view>

/* pages/page/page.wxss */
.view1 {
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: red;
    width: 600rpx;
    height: 600rpx;
}

.view2 {
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: blue;
    width: 400rpx;
    height: 400rpx;
}

.view3 {
    background-color: green;
    width: 200rpx;
    height: 200rpx;
}

// pages/page/page.js
Page({
    view1CaptureEvent() {
        console.log('view1CaptureEvent')
    },
    view2CaptureEvent() {
        console.log('view2CaptureEvent')
    },
    view3CaptureEvent() {
        console.log('view3CaptureEvent')
    }
})
    当我们点击最内层的组件时,就会触发事件冒泡,从外层向内层层层触发点击时,直到冒泡到最外层的组件为止,如下,点击最内层的结果;
view3CaptureEvent
view2CaptureEvent
view1CaptureEvent
阻止冒泡或捕获
    如果我们,我们希望阻止事件捕获或者冒泡向外或者向内继续传递,那么我们可以使用catch来实现,其实很简单,就直接把事件监听属性名的bind替换为catch即可;
<!-- pages/page/page.wxml -->
<!-- 阻止事件冒泡 -->
<view class="view1" bind:tap="view1CaptureEvent">
    <view class="view2" catch:tap="view2CaptureEvent">
        <view class="view3" bind:tap="view3CaptureEvent"></view>
    </view>
</view>

<!-- pages/page/page.wxml -->
<!-- 阻止事件捕获 -->
<view class="view1" capture-bind:tap="view1CaptureEvent">
    <view class="view2" capture-catch:tap="view2CaptureEvent">
        <view class="view3" capture-bind:tap="view3CaptureEvent"></view>
    </view>
</view>

组件化

    在小程序的开发中,我们使用的一些view、button、image等组件,都是小程序的内置组件,但是除了内置组件之外,我们其实也可以自定义组件,当我们想要开发一个较为复杂的页面时,如果希望将所有的逻辑全部都在这一个页面中处理完,会变得非常复杂,且代码可读性低下,不利于后续的维护和扩展;
    但如果将这么一个较为复杂但页面,拆分成一个一个的小的功能模块,每个功能模块完成特定的功能,然后将其进行整合,这样整个页面的维护和扩展就会变得非常的方便,这就是组件化开发的思想,不仅可以将一个较大的程序实现解耦,还可以实现功能复用,极大的加快了开发进度,小程序在刚刚推出时是不支持组件化的,但是在基础库1.6.3之后,小程序开始支持自定义组件开发;

创建组件

    小程序的组件和Vue的组件非常的相似,在小程序内,组件其实和页面的结构是一摸一样的,每一个组件内一样可以有最基础的四个文件,即wxml、wxss、js和json文件,每个组件都可以有自己独特的逻辑、样式和配置;
    那么怎么区分组件和非组件呢,这就涉及到一个配置,在每个组件下面的json文件里面的的对象内最少有一个配置,即component,当它的值为true时,表示当前当页面为一个组件页面,这样,就区分开了组件和非组件之间的区别;
    那么为了能够将组件和页面能够划分开来,以免造成混乱,所以我们一般将组件定义在项目根目录下面的一个components的目录下,然后以组件的名称创建一个文件夹,在该文件夹内创建相应的组件内容,如下示例;
[cce@doorta ~]# tree /usr/local/Project/wechat
├── app.js
├── app.json
├── app.wxss
├── components
│   └── section-info # section-info组件
│       ├── section-info.js
│       ├── section-info.json
│       ├── section-info.wxml
│       └── section-info.wxss
├── pages
│   └── index
│       ├── index.js
│       ├── index.json
│       ├── index.wxml
│       └── index.wxss
├── project.config.json
├── project.private.config.json
└── sitemap.json
[cce@doorta ~]# cat /usr/local/Project/wechat/components/section-info/section-info.json 
{
  "component": true, # 设置当前页面为组件
  "usingComponents": {}
}

组件页面注册

    那么当我们的组件定义好了之后,想要使用这个组件,那么我们必须将这个组件注册到页面当中,注意,这里不是注册到小程序当中,而是注册到页面当中,也就是说,哪个页面想要使用哪个组件,那么就必须在这个页面里面注册指定的组件;
    那么对于注册组件,就是在页面下面的json文件里面的对象内加入一个usingComponents的对象,key为组件的名称(可以不必和组件原名一致),value为组件的路径,组件路径并非组件的根目录,而是根目录下面的组件原名,如下示例;
[cce@doorta ~]#  cat /usr/local/Project/wechat/pages/index/index.json 
{
        "usingComponents": {
                "section-info": "/components/section-info/section-info"
        }
}
    那么当注册完成之后,对于组件的使用,也非常简单,和内置组件的使用方法一致,可以写单标签也可以写成双标签,如下示例;
<!--pages/index/index.wxml-->
<section-info/>

  • 注意:当自定义组件当中,也可以引用别的自定义组件,使用方法和上述一致,同时,在定义组件名时,官方不允许组件名以wx-开头;

组件全局注册

    当一个组件,可能在很多个页面都要被引用时,我们其实也可以直接将这个组件在全局进行注册,直接在项目目录下面的app.json文件内的对象加入usingComponents对象即可,配置方法和页面的配置方法一致,至此,我们就可以在全局使用这个组件,所有页面都可以直接引用,如下示例;
[cce@doorta ~]# cat /usr/local/Project/wechat/app.json 
{
  "pages": [
    "pages/index/index"
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "Weixin",
    "navigationBarTextStyle": "black",
    "enablePullDownRefresh": true
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json",
  "usingComponents": { # 全局注册
                "section-info": "/components/section-info/section-info"
        }
}

页面及组件样式

    对于页面和组件的样式我们在开发的过程中,要严格的注意它们之间的影响,主要有几点,第一,在组件内使用class作为样式选择器,值对组件内的wxml内的组件生效,对于引用的Page页面不生效;
    第二,在组件内,其实我们可以使用ID选择器、属性选择器和标签选择器,但是极不推荐使用这三类选择器,它会直接影响Page页面中组件的样式,所以在小程序内,推荐使用class作为样式选择器;
    第三,在页面内,如果我们使用标签选择器作为样式选择器,那么它会对这个页面所引用的组件产生影响,这一点要格外注意;
样式隔离
    上面说了三点对于组件和页面之间的样式影响的注意问题,那么其实对于组件的页面的样式是否需要隔离,这个是可以配置的,在每一个组件内都有一个和页面一样的js文件,该文件主要是用来编写组件的一些逻辑的,在这个js文件内有一个Component的函数,它类似页面的Page函数,同样的传入一个对象,在这个对象内,我们可以加入一个options的对象,这个对象里面有一个属性,即styleIsolation,该属性就可以用来配置组件和样式是否隔离,它的取值常用的有三个,如下;
isolated:默认值,表示启用隔离,自自定义组件内外,使用class指定的样式将不会相互影响;
apply-shared:表示页面的wxss样式将影响自定义组件,但自定义组件中的wxss中指定的样式将不会影响页面;
shared:表示页面的wxss样式将影响自定义组件,同时自定义组件的wxss样式也将影响页面;
  • 总结:综上所述,在小程序内,不推荐使用ID选择器、标签选择器和属性选择器,推荐使用class类选择器,即使我们声明了样式隔离,也没什么太大的用处,所以对于样式隔离这个配置,了解即可;

生命周期

    组件的生命周期指的是组件自身的一些钩子函数,这些函数在特殊的时间点或遇到一些特殊的框架事件时,会被自动触发,其中最重要的生命周期有三个,即created、attached、detached,这三个生命周期的钩子函数,包含了一个组件实例的生命周期流程最主要的时间点;
    组件的生命周期本身是可以直接写在组件的js文件内的Component函数里面的参数对象内的,但是自小程序基础库版本2.2.3起,组件的生命周期也可以在Component函数里面的参数对象内的lifetimes对象内进行声明,这也是目前推荐的编写方式,其优先级最高;
created:组件被创建,可以用来做网络请求初始化数据等;
attached:组件被添加到组件树当中,可以获取DOM信息;
ready:当组件在是涂层布局完成之后执行;
moved:当组件实例被移动到节点树另一个位置时执行;
detached:组件从组件树当中移除,可以做一些垃圾回收操作;
error:当组件方法抛出异常时执行;
组件所在页面的生命周期
    在小程序内,组件除了有自己的生命周期之外,还有当组件加载到页面之后的一个生命周期,这些生命周期一共有三个,它们都在Component函数里面的参数对象内的pageLifetimes对象内进行声明,没什么太大的实际用途,了解即可;
hide:组件所在页面被展示时执行;
show:组件所在页面被隐藏时执行;
resize:组件所在页面尺寸发生变化时执行

Component函数

    在小程序全局有一个App函数,在页面有一个Page函数,那么在组件中,也有一个Component实例,同样的,调用Component函数时,可以指定当前组件的属性、数据、方法等,那么一个Component实例的对象参数内,主要有几项,即properties、data、methods、options、externalClasses、observers、pageLifetunes和lefetimes,它们都是对象类型,此处仅做介绍不做过多的演示,如下;
properties:定义组件通信数据;
data:定时页面数据;
methods:自定义方法;
options:额外的配置选项;
externalClasses:引用外部样式;
observers:属性和数据监听;
pageLifetunes:组件在页面时的生命周期;
lefetimes:组件自身的生命周期;

组件通信

    很多情况下,页面展示的一些数据、样式等数据,并不一定是在页面内写死的,那么对于这一点,组件也是一样的,那么当一个组件被引用到一个页面时,由于组件内的数据需要进一步渲染,那么怎么从页面将数据传递到组件呢,其实这一点就涉及到组件通信的问题;
    其实对于小程序的组件通信和Vue的组件通信非常相似,但在小程序内,不仅可以向组件传递数据,还可以向组件传递样式、标签以及自定义事件;
数据传递
    对于组件的数据,一般来源可以有两种,第一种是在js文件里面Component函数参数对象内的data对象,和页面是一样的,只不过页面是Page函数而组件是Component函数,第二种,就是由页面来传递,那么对于页面传递数据到组件,和Vue非常相似,我们只需要在引用组件时,在组件之上加入要传递的数据即可,以属性的方式加入到组件之上,属性名为标识符,值为数据;
    那么对于组件来讲,我们需要在组件内的js文件里面的Component函数参数对象内加入一个properties对象,然后将需要接受的数据作为key即可,该key必须和组件之上的标识符一致,它的值是一个对象,该对象内,可以加入两个属性,第一个属性type,代表接受数据的类型,对于type的类型,可以有String、Number、Boolean、Object、Array和null类型,第二个属性value,表示默认值,如下示例;
<!--components/section-info/section-info.wxml-->
<text>{{title}}</text>

// components/section-info/section-info.js
Component({
  properties: {
    title: {
      type: String,
      value: "title默认值"
    }
  }
})

<!--pages/index/index.wxml-->
<section-info title="页面传递过来的title值"/>

传递样式
    在小程序内,页面可以除了可以向组件传递数据之外,还可以向组件传递样式,但是由于这种东西其实用得不是非常多,所以在此不做过多的赘述,无太大的实际用途;
传递事件
    在小程序内,组件向外传递事件,其实也和Vue非常的相似,假设,我们希望点击组件内的一个按钮,触发页面当中的一个事件处理函数,那么就涉及到自定义事件,那么这个时候,首先,我们需要在组件内部监听这个事件的点击,然后将这个事件,在组件的事件处理函数中,传递出去;
    那么对于组件向外传递事件,需要用到一个this.triggerEvent的函数,该函数接受两个参数,第一个参数为事件名称,第二个为传递给页面事件处理函数的参数,那么在页面这边,我们就需要在组件上监听这个自定义事件了,使用bind:事件名称来监听指定的事件,从而触发对应的事件处理函数;
    那么对于组件的事件就不再向页面那种编写模式了,在组件中,事件处理函数,我们需要写在methods里面,这其实就是Vue的Options API,如下示例;
<!--components/section-info/section-info.wxml-->
<button size="mini" type="primary" bind:tap="btnclick">触发事件</button>
// components/section-info/section-info.js
Component({
  methods: {
    btnclick() {
      this.triggerEvent("custom_click")
    }
  }
})

<!--pages/index/index.wxml-->
<section-info title="页面传递过来的title值" bind:custom_click="click"/>
// pages/index/index.js
Page({
  click(){
    console.log(1)
  }
})
    那么在组件的事件处理函数中,调用this.triggerEvent函数来触发自定义事件,也支持参数的传递,建议用一个对象的形式传递到页面,那么在页面这一侧,我们可以使用event.detail属性拿到组件传递过来的参数,如下示例;
// components/section-info/section-info.js
Component({
  methods: {
    btnclick() {
      this.triggerEvent("custom_click",{"name":"cce"})
    }
  }
})


// pages/index/index.js
Page({
  click(e){
    console.log(e.detail)
  }
})
# 结果如下
{name: "cce"}

组件插槽

    在开发中,我们会经常复用一个一个可复用的组件,同时,我们也可以通过properties实现间实现数据传递,让组件根据不同的数据展示不同的内容,但是这里有一个问题,就是组件就是一套模版,它里面的结构基本都是写死的、固定的;
    比如,某些情况希望组件只显示一个<button>元素, 但又因为其他原因,希望组件只显示一个<img>元素,虽然这种需求我们可以通过现有的知识去实现,但是可能比较鸡肋,那么在这种场景下,小程序框架提供了和Vue框架一样插槽功能,让调用者(父组件)来决定,组件显示什么内容;
    插槽(slot)是小程序为组件的封装者提供的一种可编程的能力,允许开发者在封装组件时,把不确定的、希望由开发者指定的部分定义为插槽,从更加简单的方式来理解,其实我们可以把插槽认为是组件封装期间,预留的占位符;
单插槽
    这里我们需要明确一个问题,插槽是预留在组件当中的一个占位符,所以插槽是依托于组件的,所以如果想要使用使用插槽,首先我们必须创建一个组件,然后在组件内预留一个插槽占位符,这个插槽占位符其实和Vue是一样的,在小程序内被称之为slot组件,Vue里面称之为slot元素,但是需要注意的是,小程序内的插槽是不支持默认值的,所以当没有插入任何东西时,插槽就不会进行渲染;
    那么在我们的组件里面定义了插槽之后,想要在页面中使用也非常简单,直接将想要插入的内容,用自定义组件包裹起来即可,这个内容将会直接替换组件内的<slot>组件,如下示例;
<!--pages/index/index.wxml-->
<view>
    <section_info>
        <button size="mini" type="primary">触发事件</button>
    </section_info>
</view>
{
    "usingComponents": {
        "section_info": "/components/section-info/section-info"
    }
}

<!--components/section-info/section-info.wxml-->
<view>
    <slot></slot>
</view>

多插槽
    上述是单插槽的使用,那么对于一个复杂的组件中,可能存在多个插槽,那么对于小程序来讲,如果想要使用多个插槽,我们需要给每一个插槽加上一个属性,即使name属性,给每一个插值命个名,然后在页面中引用这个组件时,在插入的组件上指定该组件插入到哪一个插槽内,对于这个指定的方式也非常简单,直接在内容的组件上加入一个slot属性即可,其值为需要插入的插槽的name值;
    同时,小程序的默认配置是不支持多插槽的,所以如果我们希望使用多个插槽,还需要在组件的js文件内的Component函数的对象参数里面的options对象加入multipleSlots属性,并设置该属性值为true,这样,我们才能够使用多插槽,如下示例;
<!--components/section-info/section-info.wxml-->
<view>
    <slot name="s1"></slot>
    <slot name="s2"></slot>
</view>
// components/section-info/section-info.js
Component({
  options: {
    multipleSlots: true
  }
})

<!--pages/index/index.wxml-->
<view>
    <section_info>
        <button slot="s1" size="mini" type="primary">插槽1</button>
        <button slot="s2" size="mini" type="primary">插槽2</button>
    </section_info>
</view>

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注