Skip to content

數據綁定與狀態管理

uni-app基於Vue.js開發,完全支援Vue.js的數據綁定和狀態管理特性。本文將詳細介紹在uni-app中如何進行數據綁定以及使用不同的狀態管理方案。

數據綁定基礎

數據聲明

在uni-app中,頁面的數據需要在data選項中聲明:

javascript
export default {
  data() {
    return {
      message: 'Hello uni-app',
      count: 0,
      isActive: false,
      user: {
        name: '張三',
        age: 25
      },
      list: ['蘋果', '香蕉', '橙子']
    }
  }
}

文本插值

使用雙大括號語法(Mustache語法)進行文本插值:

html
<view>{{ message }}</view>
<view>計數:{{ count }}</view>
<view>用戶名:{{ user.name }},年齡:{{ user.age }}</view>

屬性綁定

使用v-bind指令(簡寫為:)綁定HTML屬性:

html
<view :class="isActive ? 'active' : ''">動態類名</view>
<image :src="imageUrl" mode="aspectFit"></image>
<view :style="{ color: textColor, fontSize: fontSize + 'px' }">動態樣式</view>

雙向綁定

使用v-model指令實現表單元素的雙向數據綁定:

html
<input v-model="message" placeholder="請輸入內容" />
<textarea v-model="content" placeholder="請輸入詳細描述"></textarea>
<switch v-model="isChecked"></switch>
<slider v-model="sliderValue" min="0" max="100"></slider>

列表渲染

使用v-for指令渲染列表:

html
<view v-for="(item, index) in list" :key="index">
  {{ index + 1 }}. {{ item }}
</view>

<view v-for="(value, key) in user" :key="key">
  {{ key }}: {{ value }}
</view>

事件綁定

使用v-on指令(簡寫為@)綁定事件:

html
<button @click="increment">計數器 +1</button>
<view @tap="handleTap">點擊我</view>
<input @input="handleInput" />

事件處理方法定義在methods選項中:

javascript
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    },
    handleTap(event) {
      console.log('視圖被點擊', event)
    },
    handleInput(event) {
      console.log('輸入內容:', event.detail.value)
    }
  }
}

計算屬性與偵聽器

計算屬性

使用computed選項定義計算屬性:

javascript
export default {
  data() {
    return {
      price: 100,
      quantity: 2
    }
  },
  computed: {
    // 計算總價
    totalPrice() {
      return this.price * this.quantity
    },
    // 帶有getter和setter的計算屬性
    fullName: {
      get() {
        return this.firstName + ' ' + this.lastName
      },
      set(newValue) {
        const names = newValue.split(' ')
        this.firstName = names[0]
        this.lastName = names[1]
      }
    }
  }
}

在模板中使用計算屬性:

html
<view>總價:{{ totalPrice }}元</view>
<view>全名:{{ fullName }}</view>

偵聽器

使用watch選項監聽數據變化:

javascript
export default {
  data() {
    return {
      question: '',
      answer: ''
    }
  },
  watch: {
    // 監聽question變化
    question(newVal, oldVal) {
      if (newVal.trim().endsWith('?')) {
        this.getAnswer()
      }
    },
    // 深度監聽對象變化
    userInfo: {
      handler(newVal, oldVal) {
        console.log('用戶信息變化了', newVal)
      },
      deep: true
    },
    // 立即執行的偵聽器
    searchText: {
      handler(newVal) {
        this.fetchSearchResults(newVal)
      },
      immediate: true
    }
  },
  methods: {
    getAnswer() {
      this.answer = '思考中...'
      // 模擬API請求
      setTimeout(() => {
        this.answer = '這是一個回答'
      }, 1000)
    },
    fetchSearchResults(text) {
      // 搜索邏輯
    }
  }
}

組件通信

父子組件通信

  1. 父組件向子組件傳遞數據(Props)

子組件定義props:

javascript
// 子組件 child.vue
export default {
  props: {
    // 基礎類型檢查
    title: String,
    // 多種類型
    id: [String, Number],
    // 必填項
    content: {
      type: String,
      required: true
    },
    // 帶有默認值
    showFooter: {
      type: Boolean,
      default: false
    },
    // 對象/數組的默認值
    config: {
      type: Object,
      default() {
        return { theme: 'default' }
      }
    },
    // 自定義驗證函數
    priority: {
      validator(value) {
        return ['high', 'medium', 'low'].includes(value)
      }
    }
  }
}

父組件傳遞props:

html
<!-- 父組件 -->
<child-component 
  title="標題" 
  :id="itemId" 
  content="這是內容" 
  :show-footer="true"
  :config="{ theme: 'dark' }"
  priority="high"
></child-component>
  1. 子組件向父組件傳遞事件(Events)

子組件觸發事件:

javascript
// 子組件
export default {
  methods: {
    submit() {
      // 觸發自定義事件,並傳遞數據
      this.$emit('submit', { id: 1, data: 'some data' })
    }
  }
}

父組件監聽事件:

html
<!-- 父組件 -->
<child-component @submit="handleSubmit"></child-component>
javascript
// 父組件
export default {
  methods: {
    handleSubmit(data) {
      console.log('收到子組件數據', data)
    }
  }
}

跨組件通信

  1. 使用事件總線(EventBus)

創建事件總線:

javascript
// eventBus.js
import Vue from 'vue'
export const eventBus = new Vue()

// 在Vue 3中可以使用mitt庫
// import mitt from 'mitt'
// export const eventBus = mitt()

組件A發送事件:

javascript
// 組件A
import { eventBus } from '@/utils/eventBus'

export default {
  methods: {
    sendMessage() {
      eventBus.$emit('message', '這是來自組件A的消息')
    }
  }
}

組件B接收事件:

javascript
// 組件B
import { eventBus } from '@/utils/eventBus'

export default {
  data() {
    return {
      message: ''
    }
  },
  created() {
    // 監聽事件
    eventBus.$on('message', this.receiveMessage)
  },
  beforeDestroy() {
    // 移除監聽
    eventBus.$off('message', this.receiveMessage)
  },
  methods: {
    receiveMessage(msg) {
      this.message = msg
    }
  }
}
  1. 使用provide/inject

祖先組件提供數據:

javascript
// 祖先組件
export default {
  provide() {
    return {
      theme: this.theme,
      // 提供方法
      updateTheme: this.updateTheme
    }
  },
  data() {
    return {
      theme: 'light'
    }
  },
  methods: {
    updateTheme(newTheme) {
      this.theme = newTheme
    }
  }
}

後代組件注入數據:

javascript
// 後代組件
export default {
  inject: ['theme', 'updateTheme'],
  methods: {
    changeTheme() {
      this.updateTheme('dark')
    }
  }
}

狀態管理

全局變量

對於簡單應用,可以使用全局變量進行狀態管理:

javascript
// App.vue
export default {
  globalData: {
    userInfo: null,
    token: '',
    settings: {}
  },
  onLaunch() {
    // 初始化全局數據
  },
  methods: {
    // 全局方法
    updateUserInfo(userInfo) {
      this.globalData.userInfo = userInfo
    }
  }
}

在頁面或組件中使用:

javascript
// 頁面或組件
export default {
  methods: {
    getUserInfo() {
      const app = getApp()
      return app.globalData.userInfo
    },
    login() {
      // 登錄成功後更新全局數據
      getApp().updateUserInfo({ name: '張三', id: '123' })
    }
  }
}

Vuex狀態管理

對於複雜應用,推薦使用Vuex進行狀態管理:

  1. 安裝Vuex
bash
npm install vuex --save
  1. 創建Store
javascript
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    hasLogin: false,
    userInfo: {},
    cartItems: []
  },
  getters: {
    cartCount(state) {
      return state.cartItems.length
    },
    isVip(state) {
      return state.userInfo.vip === true
    }
  },
  mutations: {
    login(state, userInfo) {
      state.hasLogin = true
      state.userInfo = userInfo
    },
    logout(state) {
      state.hasLogin = false
      state.userInfo = {}
    },
    addToCart(state, item) {
      state.cartItems.push(item)
    }
  },
  actions: {
    // 異步操作
    loginAction({ commit }, username) {
      return new Promise((resolve, reject) => {
        // 模擬API請求
        setTimeout(() => {
          const userInfo = { name: username, id: Date.now() }
          commit('login', userInfo)
          resolve(userInfo)
        }, 1000)
      })
    },
    // 帶有條件判斷的action
    checkoutAction({ state, commit }, payload) {
      if (state.cartItems.length === 0) {
        return Promise.reject('購物車為空')
      }
      
      // 結賬邏輯
      return api.checkout(state.cartItems)
        .then(() => {
          commit('clearCart')
          return '結賬成功'
        })
    }
  },
  modules: {
    // 模塊化狀態管理
    products: {
      namespaced: true,
      state: { list: [] },
      mutations: { /* ... */ },
      actions: { /* ... */ }
    },
    orders: {
      namespaced: true,
      state: { list: [] },
      mutations: { /* ... */ },
      actions: { /* ... */ }
    }
  }
})
  1. 在main.js中掛載Store
javascript
// main.js
import Vue from 'vue'
import App from './App'
import store from './store'

Vue.prototype.$store = store

const app = new Vue({
  store,
  ...App
})
app.$mount()
  1. 在組件中使用Vuex
javascript
// 組件中
export default {
  computed: {
    // 映射state
    ...Vuex.mapState(['hasLogin', 'userInfo']),
    // 映射getters
    ...Vuex.mapGetters(['cartCount', 'isVip']),
    // 自定義計算屬性
    welcomeMessage() {
      return this.hasLogin ? `歡迎回來,${this.userInfo.name}` : '請登錄'
    }
  },
  methods: {
    // 映射mutations
    ...Vuex.mapMutations(['logout', 'addToCart']),
    // 映射actions
    ...Vuex.mapActions(['loginAction', 'checkoutAction']),
    // 使用示例
    async handleLogin() {
      try {
        const userInfo = await this.loginAction('張三')
        uni.showToast({ title: '登錄成功' })
      } catch (error) {
        uni.showToast({ title: '登錄失敗', icon: 'none' })
      }
    },
    // 使用命名空間的模塊
    loadProducts() {
      this.$store.dispatch('products/loadList')
    }
  }
}

Pinia狀態管理(Vue 3)

如果您使用的是Vue 3版本的uni-app,可以使用更現代的Pinia進行狀態管理:

  1. 安裝Pinia
bash
npm install pinia --save
  1. 創建Store
javascript
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    hasLogin: false,
    userInfo: {}
  }),
  getters: {
    isVip: (state) => state.userInfo.vip === true,
    fullName: (state) => {
      return state.userInfo.firstName + ' ' + state.userInfo.lastName
    }
  },
  actions: {
    async login(username, password) {
      // 模擬API請求
      const userInfo = await api.login(username, password)
      this.hasLogin = true
      this.userInfo = userInfo
      return userInfo
    },
    logout() {
      this.hasLogin = false
      this.userInfo = {}
    }
  }
})

// stores/cart.js
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  getters: {
    count: (state) => state.items.length,
    totalPrice: (state) => {
      return state.items.reduce((total, item) => {
        return total + item.price * item.quantity
      }, 0)
    }
  },
  actions: {
    addItem(item) {
      this.items.push(item)
    },
    removeItem(id) {
      const index = this.items.findIndex(item => item.id === id)
      if (index !== -1) {
        this.items.splice(index, 1)
      }
    },
    async checkout() {
      if (this.items.length === 0) {
        throw new Error('購物車為空')
      }
      
      await api.checkout(this.items)
      this.items = []
    }
  }
})
  1. 在main.js中掛載Pinia
javascript
// main.js
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

export function createApp() {
  const app = createSSRApp(App)
  const pinia = createPinia()
  app.use(pinia)
  return {
    app
  }
}
  1. 在組件中使用Pinia
vue
<template>
  <view>
    <view v-if="userStore.hasLogin">
      歡迎回來,{{ userStore.userInfo.name }}
      <button @click="logout">退出登錄</button>
    </view>
    <view v-else>
      <button @click="login">登錄</button>
    </view>
    
    <view>購物車: {{ cartStore.count }}件商品</view>
    <view>總價: {{ cartStore.totalPrice }}元</view>
    <button @click="addRandomItem">添加商品</button>
    <button @click="checkout">結賬</button>
  </view>
</template>

<script setup>
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'

const userStore = useUserStore()
const cartStore = useCartStore()

function login() {
  userStore.login('張三', '123456')
    .then(() => {
      uni.showToast({ title: '登錄成功' })
    })
    .catch(() => {
      uni.showToast({ title: '登錄失敗', icon: 'none' })
    })
}

function logout() {
  userStore.logout()
  uni.showToast({ title: '已退出登錄' })
}

function addRandomItem() {
  const id = Math.floor(Math.random() * 1000)
  cartStore.addItem({
    id,
    name: `商品${id}`,
    price: Math.floor(Math.random() * 100) + 1,
    quantity: 1
  })
}

function checkout() {
  cartStore.checkout()
    .then(() => {
      uni.showToast({ title: '結賬成功' })
    })
    .catch((error) => {
      uni.showToast({ title: error.message, icon: 'none' })
    })
}
</script>

持久化狀態

為了在應用重啟後保持狀態,可以將狀態持久化到本地存儲:

手動持久化

javascript
// 保存狀態
uni.setStorageSync('userInfo', JSON.stringify(this.userInfo))

// 恢復狀態
try {
  const userInfo = JSON.parse(uni.getStorageSync('userInfo') || '{}')
  this.userInfo = userInfo
} catch (e) {
  console.error('解析用戶信息失敗', e)
}

Vuex持久化

使用插件實現Vuex狀態自動持久化:

javascript
// store/plugins/persistedState.js
import createPersistedState from 'vuex-persistedstate'

const persistedState = createPersistedState({
  storage: {
    getItem: key => uni.getStorageSync(key),
    setItem: (key, value) => uni.setStorageSync(key, value),
    removeItem: key => uni.removeStorageSync(key)
  },
  // 只持久化部分狀態
  paths: ['hasLogin', 'userInfo', 'token']
})

export default persistedState

在Store中使用插件:

javascript
// store/index.js
import persistedState from './plugins/persistedState'

export default new Vuex.Store({
  // ...state, mutations, actions
  plugins: [persistedState]
})

Pinia持久化

使用pinia-plugin-persistedstate插件:

javascript
// main.js
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

export function createApp() {
  const app = createSSRApp(App)
  const pinia = createPinia()
  pinia.use(piniaPluginPersistedstate)
  app.use(pinia)
  return {
    app
  }
}

在Store中配置持久化:

javascript
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    hasLogin: false,
    userInfo: {}
  }),
  // 持久化配置
  persist: {
    enabled: true,
    strategies: [
      {
        key: 'user-store',
        storage: {
          getItem: (key) => uni.getStorageSync(key),
          setItem: (key, value) => uni.setStorageSync(key, value),
          removeItem: (key) => uni.removeStorageSync(key)
        },
        paths: ['hasLogin', 'userInfo'] // 只持久化部分狀態
      }
    ]
  },
  // getters, actions...
})

最佳實踐

1. 合理劃分狀態

  • 組件內狀態:僅在組件內使用的狀態,放在組件的data
  • 共享狀態:多個組件共享的狀態,放在Vuex/Pinia中
  • 持久化狀態:需要在應用重啟後保持的狀態,配置持久化

2. 避免過度使用計算屬性

計算屬性會緩存結果,但過度使用會增加內存佔用:

javascript
// 不推薦
computed: {
  item1() { return this.list[0] },
  item2() { return this.list[1] },
  item3() { return this.list[2] }
}

// 推薦
computed: {
  firstThreeItems() {
    return this.list.slice(0, 3)
  }
}

3. 使用函數式更新

對於複雜狀態,使用函數式更新可以避免意外修改:

javascript
// 不推薦
this.userInfo.age += 1

// 推薦
this.userInfo = {
  ...this.userInfo,
  age: this.userInfo.age + 1
}

// Vuex中
mutations: {
  updateUserAge(state, age) {
    state.userInfo = {
      ...state.userInfo,
      age
    }
  }
}

4. 模塊化狀態管理

對於大型應用,將狀態分模塊管理:

javascript
// Vuex模塊化
modules: {
  user: userModule,
  product: productModule,
  order: orderModule,
  cart: cartModule
}

// Pinia天然支援模塊化
const useUserStore = defineStore('user', { /* ... */ })
const useProductStore = defineStore('product', { /* ... */ })
const useOrderStore = defineStore('order', { /* ... */ })
const useCartStore = defineStore('cart', { /* ... */ })

5. 性能優化

  • 避免深層嵌套:狀態結構盡量扁平化
  • 使用不可變數據:修改數據時創建新對象而不是直接修改
  • 合理使用getters:將複雜計算邏輯放在getters中
  • 按需加載模塊:使用動態導入減少初始加載時間

總結

uni-app提供了完整的數據綁定和狀態管理能力,從簡單的組件內狀態到複雜的全局狀態管理都有對應的解決方案。在實際開發中,應根據應用的複雜度選擇合適的狀態管理方式,並遵循最佳實踐,以確保應用的可維護性和性能。

一次開發,多端部署 - 讓跨平台開發更簡單