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提供了完整的数据绑定和状态管理能力,从简单的组件内状态到复杂的全局状态管理都有对应的解决方案。在实际开发中,应根据应用的复杂度选择合适的状态管理方式,并遵循最佳实践,以确保应用的可维护性和性能。

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