Event Handling
Event handling is a crucial mechanism for implementing user interactions in uni-app. This guide will detail event handling methods, event types, and best practices in uni-app.
Event Binding
Basic Syntax
uni-app follows Vue's event handling syntax, using the v-on
directive (shorthand @
) to listen to DOM events:
<!-- Full syntax -->
<button v-on:tap="handleTap">Click me</button>
<!-- Shorthand syntax -->
<button @tap="handleTap">Click me</button>
Event handler methods are defined in the component's methods
option:
export default {
methods: {
handleTap() {
console.log('Button was clicked')
}
}
}
Inline Event Handlers
For simple event handling logic, you can use inline JavaScript statements directly in templates:
<button @tap="counter += 1">Counter: {{ counter }}</button>
Event Parameters
You can pass custom parameters to event handler methods:
<button @tap="handleTap('hello', $event)">Event with parameters</button>
methods: {
handleTap(message, event) {
console.log(message) // 'hello'
console.log(event) // Native event object
}
}
TIP
Use $event
to access the native event object while passing custom parameters.
Common Event Types
uni-app supports various event types. Here are some commonly used events:
Touch Events
- tap: Click event, similar to HTML's click event
- longpress: Long press event, triggered when finger touches for a long time
- touchstart: Touch start event
- touchmove: Touch move event
- touchend: Touch end event
- touchcancel: Touch cancel event
<view @tap="handleTap">Click</view>
<view @longpress="handleLongPress">Long press</view>
<view
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
Touch area
</view>
Form Events
- input: Triggered when input content changes
- focus: Triggered when input gains focus
- blur: Triggered when input loses focus
- change: Triggered when picker, switch values change
- submit: Triggered when form is submitted
<input
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
placeholder="Enter content"
/>
<picker
@change="handleChange"
:range="['Option 1', 'Option 2', 'Option 3']"
>
<view>Current selection: {{ currentSelection }}</view>
</picker>
<form @submit="handleSubmit">
<!-- Form content -->
<button form-type="submit">Submit</button>
</form>
Lifecycle Events
- load: Triggered when page loads
- ready: Triggered when page renders for the first time
- show: Triggered when page shows
- hide: Triggered when page hides
These events are typically handled in page lifecycle hooks:
export default {
onLoad(options) {
console.log('Page loaded', options)
},
onReady() {
console.log('Page rendered for the first time')
},
onShow() {
console.log('Page shown')
},
onHide() {
console.log('Page hidden')
}
}
Scroll Events
- scroll: Triggered when scrolling
<scroll-view
scroll-y
@scroll="handleScroll"
style="height: 300px;"
>
<view v-for="item in items" :key="item.id">
{{ item.text }}
</view>
</scroll-view>
methods: {
handleScroll(e) {
console.log('Scroll position', e.detail)
// e.detail.scrollTop - vertical scroll position
// e.detail.scrollLeft - horizontal scroll position
// e.detail.scrollHeight - scroll content height
// e.detail.scrollWidth - scroll content width
}
}
Event Modifiers
uni-app supports Vue's event modifiers for handling event details:
Event Propagation Modifiers
- .stop: Prevent event bubbling
- .prevent: Prevent default event behavior
- .capture: Use event capture mode
- .self: Only trigger handler when event is triggered on the element itself
- .once: Event triggers only once
<!-- Prevent event bubbling -->
<view @tap.stop="handleTap">Prevent bubbling</view>
<!-- Prevent default behavior -->
<form @submit.prevent="handleSubmit">
<!-- Form content -->
</form>
<!-- Trigger only once -->
<button @tap.once="handleTap">Can only be clicked once</button>
Key Modifiers
When handling keyboard events, you can use key modifiers:
<!-- Submit form when Enter is pressed -->
<input @keyup.enter="submit" />
<!-- Cancel operation when Esc is pressed -->
<input @keyup.esc="cancel" />
Note
Key modifiers are mainly effective on H5 platform. On mini-program and App platforms, you may need to use other methods to handle keyboard events.
Event Object
Event handler functions automatically receive an event object containing event-related information:
<view @tap="handleTap">Click to get event info</view>
methods: {
handleTap(event) {
console.log(event)
// Common properties
console.log(event.type) // Event type, e.g., 'tap'
console.log(event.target) // Element that triggered the event
console.log(event.currentTarget) // Element currently handling the event
console.log(event.timeStamp) // Event trigger timestamp
// Touch event specific properties
if (event.touches) {
console.log(event.touches) // All touch points on screen
console.log(event.changedTouches) // Touch points that triggered current event
}
// Form event specific properties
if (event.detail && event.detail.value !== undefined) {
console.log(event.detail.value) // Form component value
}
}
}
Cross-platform Event Object Differences
Event objects may differ across platforms (mini-programs, H5, App). It's recommended to use the following approach for universal properties:
methods: {
handleInput(event) {
// Get input value
const value = event.detail.value || event.target.value
// Get dataset
const dataset = event.currentTarget.dataset
}
}
Custom Events
Component Communication
In custom components, you can use the $emit
method to trigger custom events for child-to-parent communication:
Child component (child.vue):
<template>
<view>
<button @tap="sendMessage">Send Message</button>
</view>
</template>
<script>
export default {
methods: {
sendMessage() {
// Trigger custom event and pass data
this.$emit('message', {
content: 'This is a message from child component',
time: new Date()
})
}
}
}
</script>
Parent component:
<template>
<view>
<!-- Listen to child component's custom event -->
<child @message="handleMessage"></child>
<view v-if="messageReceived">
Received message: {{ message.content }}
Time: {{ formatTime(message.time) }}
</view>
</view>
</template>
<script>
import Child from './child.vue'
export default {
components: {
Child
},
data() {
return {
messageReceived: false,
message: null
}
},
methods: {
handleMessage(msg) {
this.messageReceived = true
this.message = msg
},
formatTime(date) {
return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`
}
}
}
</script>
Event Bus
For communication between non-parent-child components, you can use an event bus:
// eventBus.js
import Vue from 'vue'
export const eventBus = new Vue()
// For Vue 3, you can use mitt library
// import mitt from 'mitt'
// export const eventBus = mitt()
Component A (sending events):
import { eventBus } from '@/utils/eventBus'
export default {
methods: {
sendGlobalMessage() {
eventBus.$emit('global-message', {
from: 'ComponentA',
content: 'Global message'
})
}
}
}
Component B (receiving events):
import { eventBus } from '@/utils/eventBus'
export default {
data() {
return {
messages: []
}
},
created() {
// Listen to global events
eventBus.$on('global-message', this.receiveMessage)
},
beforeDestroy() {
// Remove event listener before component destruction
eventBus.$off('global-message', this.receiveMessage)
},
methods: {
receiveMessage(msg) {
this.messages.push(msg)
}
}
}
Gesture Recognition
uni-app provides basic touch events, but for complex gesture recognition (like swipe, pinch, rotate), you can use the following methods:
Custom Gesture Recognition
<template>
<view
class="gesture-area"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<text>{{ gestureInfo }}</text>
</view>
</template>
<script>
export default {
data() {
return {
gestureInfo: 'Please perform gestures in this area',
startX: 0,
startY: 0,
endX: 0,
endY: 0,
startTime: 0,
isSwiping: false
}
},
methods: {
handleTouchStart(e) {
const touch = e.touches[0]
this.startX = touch.clientX
this.startY = touch.clientY
this.startTime = Date.now()
this.isSwiping = true
},
handleTouchMove(e) {
if (!this.isSwiping) return
const touch = e.touches[0]
this.endX = touch.clientX
this.endY = touch.clientY
// Calculate movement distance
const deltaX = this.endX - this.startX
const deltaY = this.endY - this.startY
// Show real-time movement info
this.gestureInfo = `Moving: X=${deltaX.toFixed(2)}, Y=${deltaY.toFixed(2)}`
},
handleTouchEnd(e) {
if (!this.isSwiping) return
this.isSwiping = false
// Calculate final movement distance and direction
const deltaX = this.endX - this.startX
const deltaY = this.endY - this.startY
const duration = Date.now() - this.startTime
// Determine gesture type
if (Math.abs(deltaX) > 50 || Math.abs(deltaY) > 50) {
// Determine direction
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Horizontal direction
const direction = deltaX > 0 ? 'Right' : 'Left'
this.gestureInfo = `Horizontal swipe: ${direction}, Distance: ${Math.abs(deltaX).toFixed(2)}, Time: ${duration}ms`
} else {
// Vertical direction
const direction = deltaY > 0 ? 'Down' : 'Up'
this.gestureInfo = `Vertical swipe: ${direction}, Distance: ${Math.abs(deltaY).toFixed(2)}, Time: ${duration}ms`
}
} else if (duration < 300 && Math.abs(deltaX) < 10 && Math.abs(deltaY) < 10) {
this.gestureInfo = 'Tap'
} else {
this.gestureInfo = 'Unrecognized gesture'
}
}
}
}
</script>
<style>
.gesture-area {
width: 100%;
height: 300rpx;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
font-size: 28rpx;
}
</style>
Event Delegation
Event delegation is a common event handling pattern that improves performance and simplifies code by adding event listeners to parent elements instead of each child element:
<template>
<view class="list" @tap="handleItemClick">
<view
v-for="item in items"
:key="item.id"
class="list-item"
:data-id="item.id"
>
{{ item.text }}
</view>
</view>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
{ id: 4, text: 'Item 4' },
{ id: 5, text: 'Item 5' }
]
}
},
methods: {
handleItemClick(e) {
// Get dataset of clicked element
const dataset = e.target.dataset || e.currentTarget.dataset
if (dataset.id) {
const id = Number(dataset.id)
const item = this.items.find(item => item.id === id)
if (item) {
uni.showToast({
title: `Clicked: ${item.text}`,
icon: 'none'
})
}
}
}
}
}
</script>
Performance Optimization
Debounce and Throttle
For frequently triggered events (like scroll, input, window resize), use debounce or throttle techniques to optimize performance:
// utils/event.js
// Debounce function
export function debounce(func, wait = 300) {
let timeout
return function(...args) {
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// Throttle function
export function throttle(func, wait = 300) {
let timeout = null
let previous = 0
return function(...args) {
const now = Date.now()
const remaining = wait - (now - previous)
if (remaining <= 0) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
func.apply(this, args)
} else if (!timeout) {
timeout = setTimeout(() => {
previous = Date.now()
timeout = null
func.apply(this, args)
}, remaining)
}
}
}
Using in components:
<template>
<view>
<input @input="handleInput" placeholder="Search..." />
<scroll-view
scroll-y
@scroll="handleScroll"
style="height: 300px;"
>
<view v-for="item in items" :key="item.id">
{{ item.text }}
</view>
</scroll-view>
</view>
</template>
<script>
import { debounce, throttle } from '@/utils/event'
export default {
data() {
return {
items: [],
searchText: ''
}
},
created() {
// Create debounced and throttled functions
this.debouncedSearch = debounce(this.search, 500)
this.throttledScroll = throttle(this.onScroll, 200)
},
methods: {
// Use debounce for input events
handleInput(e) {
const value = e.detail.value || e.target.value
this.searchText = value
this.debouncedSearch(value)
},
search(text) {
console.log('Executing search:', text)
// Actual search logic
},
// Use throttle for scroll events
handleScroll(e) {
this.throttledScroll(e)
},
onScroll(e) {
console.log('Handling scroll:', e.detail.scrollTop)
// Actual scroll handling logic
}
}
}
</script>
Avoid Inline Functions
Using inline functions in templates creates new function instances on every re-render, which should be avoided:
<!-- Not recommended -->
<view v-for="item in items" :key="item.id" @tap="() => handleItemClick(item.id)">
{{ item.text }}
</view>
<!-- Recommended -->
<view v-for="item in items" :key="item.id" @tap="handleItemClick" :data-id="item.id">
{{ item.text }}
</view>
methods: {
handleItemClick(e) {
const id = e.currentTarget.dataset.id
// Handle click logic
}
}
Use Computed Properties Instead of Methods
For data transformations used multiple times in templates, use computed properties instead of methods:
<!-- Not recommended -->
<view v-for="item in items" :key="item.id">
{{ formatPrice(item.price) }}
</view>
<!-- Recommended -->
<view v-for="item in formattedItems" :key="item.id">
{{ item.formattedPrice }}
</view>
computed: {
formattedItems() {
return this.items.map(item => ({
...item,
formattedPrice: this.formatPrice(item.price)
}))
}
},
methods: {
formatPrice(price) {
return '$' + price.toFixed(2)
}
}
Cross-platform Considerations
Event Naming Differences
Different platforms may have different event naming conventions:
- Web platform uses
click
, while mini-programs usetap
- Web platform uses
change
, while some mini-program components may usebindchange
For consistency, uni-app provides unified handling. It's recommended to use uni-app's recommended event names:
<!-- Use tap event on all platforms -->
<view @tap="handleTap">Click</view>
<!-- Use change event on all platforms -->
<picker @change="handleChange" :range="options">Select</picker>
Event Object Differences
Event object structures may differ across platforms:
- Web platform gets input value through
event.target.value
- Mini-programs get input value through
event.detail.value
To handle these differences, write compatibility code:
methods: {
handleInput(event) {
// Compatible with different platforms
const value = event.detail.value || (event.target && event.target.value) || ''
this.inputValue = value
}
}
Event Bubbling Differences
Event bubbling mechanisms may differ across platforms, especially with nested custom components:
<!-- Parent component -->
<view @tap="handleOuterTap">
<custom-component @tap="handleInnerTap"></custom-component>
</view>
On some platforms, clicking the custom component might trigger both inner and outer tap events. To ensure consistent behavior, use the .stop
modifier:
<view @tap="handleOuterTap">
<custom-component @tap.stop="handleInnerTap"></custom-component>
</view>
Practical Examples
Pull-to-Refresh and Load More
<template>
<view class="container">
<!-- Custom pull-to-refresh -->
<view
class="refresh-container"
:style="{ height: refreshHeight + 'px' }"
:class="{ 'refreshing': isRefreshing }"
>
<view class="refresh-icon" :class="{ 'rotate': isRefreshing }">↓</view>
<text>{{ refreshText }}</text>
</view>
<!-- Content area -->
<scroll-view
scroll-y
class="scroll-view"
@scrolltoupper="handleScrollToUpper"
@scrolltolower="handleScrollToLower"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
:style="{ transform: `translateY(${translateY}px)` }"
>
<view class="list">
<view
v-for="item in list"
:key="item.id"
class="list-item"
>
<text class="item-title">{{ item.title }}</text>
<text class="item-desc">{{ item.description }}</text>
</view>
</view>
<!-- Load more -->
<view class="loading-more" v-if="hasMore || isLoadingMore">
<view class="loading-icon" v-if="isLoadingMore"></view>
<text>{{ loadingMoreText }}</text>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
list: [],
page: 1,
pageSize: 10,
hasMore: true,
isLoadingMore: false,
isRefreshing: false,
// Pull-to-refresh related
startY: 0,
moveY: 0,
translateY: 0,
refreshHeight: 0,
maxRefreshHeight: 80,
refreshThreshold: 50,
isTouching: false
}
},
computed: {
refreshText() {
if (this.isRefreshing) {
return 'Refreshing...'
}
return this.refreshHeight >= this.refreshThreshold ? 'Release to refresh' : 'Pull to refresh'
},
loadingMoreText() {
if (!this.hasMore) {
return 'No more data'
}
return this.isLoadingMore ? 'Loading more...' : 'Pull up to load more'
}
},
created() {
// Initial data load
this.loadData()
},
methods: {
// Load data
loadData(isRefresh = false) {
if (isRefresh) {
this.page = 1
this.hasMore = true
}
// Simulate request
setTimeout(() => {
const newData = Array.from({ length: this.pageSize }, (_, index) => {
const itemIndex = (this.page - 1) * this.pageSize + index + 1
return {
id: `item-${this.page}-${index}`,
title: `Title ${itemIndex}`,
description: `This is the detailed description for item ${itemIndex}, containing some related content.`
}
})
if (isRefresh) {
this.list = newData
} else {
this.list = [...this.list, ...newData]
}
// Check if there's more data
if (this.page >= 5) {
this.hasMore = false
} else {
this.page++
}
// Reset states
this.isRefreshing = false
this.isLoadingMore = false
// Reset position if refreshing
if (isRefresh) {
this.resetRefresh()
}
}, 1000)
},
// Pull-to-refresh related methods
handleTouchStart(e) {
// Only allow pull-to-refresh at the top
if (e.touches[0] && !this.isRefreshing) {
this.startY = e.touches[0].clientY
this.isTouching = true
}
},
handleTouchMove(e) {
if (!this.isTouching || this.isRefreshing) return
this.moveY = e.touches[0].clientY
let distance = this.moveY - this.startY
// Only trigger refresh on pull down
if (distance <= 0) {
this.translateY = 0
this.refreshHeight = 0
return
}
// Add damping effect
distance = Math.pow(distance, 0.8)
// Limit maximum pull distance
if (distance > this.maxRefreshHeight) {
distance = this.maxRefreshHeight
}
this.translateY = distance
this.refreshHeight = distance
},
handleTouchEnd() {
if (!this.isTouching || this.isRefreshing) return
this.isTouching = false
// Trigger refresh if threshold is reached
if (this.refreshHeight >= this.refreshThreshold) {
this.isRefreshing = true
this.translateY = this.refreshThreshold
this.refreshHeight = this.refreshThreshold
// Execute refresh
this.loadData(true)
} else {
// Reset position if threshold not reached
this.resetRefresh()
}
},
resetRefresh() {
this.translateY = 0
this.refreshHeight = 0
},
// Scroll event handlers
handleScrollToUpper() {
console.log('Reached top')
},
handleScrollToLower() {
if (this.hasMore && !this.isLoadingMore) {
console.log('Reached bottom, loading more')
this.isLoadingMore = true
this.loadData()
}
}
}
}
</script>
<style>
.container {
height: 100vh;
position: relative;
overflow: hidden;
}
.refresh-container {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 0;
transition: height 0.2s;
overflow: hidden;
background-color: #f5f5f5;
z-index: 1;
}
.refresh-icon {
font-size: 32rpx;
margin-right: 10rpx;
transition: transform 0.3s;
}
.rotate {
animation: rotating 1s linear infinite;
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.scroll-view {
height: 100%;
transition: transform 0.2s;
}
.list {
padding: 20rpx;
}
.list-item {
background-color: #ffffff;
border-radius: 8rpx;
padding: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
.item-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 10rpx;
display: block;
}
.item-desc {
font-size: 28rpx;
color: #666;
display: block;
}
.loading-more {
text-align: center;
padding: 20rpx 0;
color: #999;
font-size: 24rpx;
display: flex;
justify-content: center;
align-items: center;
}
.loading-icon {
width: 30rpx;
height: 30rpx;
border: 2rpx solid #ccc;
border-top-color: #666;
border-radius: 50%;
margin-right: 10rpx;
animation: rotating 1s linear infinite;
}
</style>
Summary
Event handling is an essential part of uni-app development. Mastering event handling mechanisms helps developers build more interactive applications with better user experience. This guide covered event binding syntax, common event types, event modifiers, event objects, custom events, gesture recognition, and provided practical examples.
Key points to remember in actual development:
Choose appropriate event types: Select suitable event types based on interaction needs, such as
tap
for clicks,longpress
for long presses.Consider cross-platform differences: Different platforms (mini-programs, H5, App) may have different event handling mechanisms. Write code with compatibility in mind.
Optimize performance: Use debounce or throttle techniques for frequently triggered events; avoid inline functions in templates; use computed properties instead of methods when appropriate.
Organize code properly: Break down complex event handling logic into multiple methods to improve code readability and maintainability.
By properly using event handling mechanisms, you can build uni-app applications with smooth interactions and excellent user experience.