Skip to content

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:

html
<!-- 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:

javascript
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:

html
<button @tap="counter += 1">Counter: {{ counter }}</button>

Event Parameters

You can pass custom parameters to event handler methods:

html
<button @tap="handleTap('hello', $event)">Event with parameters</button>
javascript
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
html
<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
html
<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:

javascript
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
html
<scroll-view 
  scroll-y 
  @scroll="handleScroll" 
  style="height: 300px;"
>
  <view v-for="item in items" :key="item.id">
    {{ item.text }}
  </view>
</scroll-view>
javascript
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
html
<!-- 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:

html
<!-- 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:

html
<view @tap="handleTap">Click to get event info</view>
javascript
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:

javascript
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):

html
<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:

html
<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:

javascript
// 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):

javascript
import { eventBus } from '@/utils/eventBus'

export default {
  methods: {
    sendGlobalMessage() {
      eventBus.$emit('global-message', {
        from: 'ComponentA',
        content: 'Global message'
      })
    }
  }
}

Component B (receiving events):

javascript
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

html
<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:

html
<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:

javascript
// 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:

html
<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:

html
<!-- 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>
javascript
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:

html
<!-- 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>
javascript
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 use tap
  • Web platform uses change, while some mini-program components may use bindchange

For consistency, uni-app provides unified handling. It's recommended to use uni-app's recommended event names:

html
<!-- 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:

javascript
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:

html
<!-- 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:

html
<view @tap="handleOuterTap">
  <custom-component @tap.stop="handleInnerTap"></custom-component>
</view>

Practical Examples

Pull-to-Refresh and Load More

html
<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:

  1. Choose appropriate event types: Select suitable event types based on interaction needs, such as tap for clicks, longpress for long presses.

  2. Consider cross-platform differences: Different platforms (mini-programs, H5, App) may have different event handling mechanisms. Write code with compatibility in mind.

  3. Optimize performance: Use debounce or throttle techniques for frequently triggered events; avoid inline functions in templates; use computed properties instead of methods when appropriate.

  4. 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.

一次开发,多端部署 - 让跨平台开发更简单