【Vue】状态管理(Vuex、Pinia)

在这里插入图片描述

个人主页:Guiat
归属专栏:Vue

在这里插入图片描述

正文

1. 状态管理概述

1.1 什么是状态管理

在Vue应用中,状态管理是指对应用中各种数据状态的集中式管理。随着应用规模的增长,组件之间共享状态变得越来越复杂,简单的父子组件通信方式可能不再适用。状态管理工具提供了一种集中式存储管理应用所有组件的状态的方式。

1.2 为什么需要状态管理

当应用变得复杂时,我们面临以下挑战:

  • 多个视图依赖同一状态
  • 来自不同视图的行为需要变更同一状态
  • 组件层级深,组件间通信变得困难
  • 全局状态难以追踪变化来源

状态管理工具解决了这些问题,提供了:

  • 集中存储共享状态
  • 可预测的状态变更机制
  • 开发工具支持的调试能力
  • 模块化的状态管理方案

2. Vuex基础

2.1 Vuex核心概念

Vuex是Vue官方的状态管理模式,它采用集中式存储管理应用的所有组件的状态。

2.1.1 State

State是存储应用状态的地方:

// store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    count: 0,
    todos: [],
    user: null
  }
})

在组件中访问状态:

// Vue 2
export default {
  computed: {
    count() {
      return this.$store.state.count
    }
  }
}

// Vue 3
import { computed } from 'vue'
import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()
    const count = computed(() => store.state.count)
    
    return { count }
  }
}

2.1.2 Getters

Getters用于从store的state中派生出一些状态:

// store/index.js
export default createStore({
  state: {
    todos: [
      { id: 1, text: 'Learn Vue', done: true },
      { id: 2, text: 'Learn Vuex', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    },
    doneTodosCount: (state, getters) => {
      return getters.doneTodos.length
    }
  }
})

在组件中使用getters:

// Vue 2
export default {
  computed: {
    doneTodosCount() {
      return this.$store.getters.doneTodosCount
    }
  }
}

// Vue 3
import { computed } from 'vue'
import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()
    const doneTodosCount = computed(() => store.getters.doneTodosCount)
    
    return { doneTodosCount }
  }
}

2.1.3 Mutations

Mutations是更改store中状态的唯一方法:

// store/index.js
export default createStore({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    },
    incrementBy(state, payload) {
      state.count += payload.amount
    }
  }
})

在组件中提交mutations:

// Vue 2
export default {
  methods: {
    increment() {
      this.$store.commit('increment')
    },
    incrementBy(amount) {
      this.$store.commit('incrementBy', { amount })
    }
  }
}

// Vue 3
import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()
    
    function increment() {
      store.commit('increment')
    }
    
    function incrementBy(amount) {
      store.commit('incrementBy', { amount })
    }
    
    return { increment, incrementBy }
  }
}

2.1.4 Actions

Actions用于处理异步操作:

// store/index.js
export default createStore({
  state: {
    todos: []
  },
  mutations: {
    setTodos(state, todos) {
      state.todos = todos
    },
    addTodo(state, todo) {
      state.todos.push(todo)
    }
  },
  actions: {
    async fetchTodos({ commit }) {
      try {
        const response = await fetch('/api/todos')
        const todos = await response.json()
        commit('setTodos', todos)
      } catch (error) {
        console.error('Error fetching todos:', error)
      }
    },
    async addTodo({ commit }, todoText) {
      try {
        const response = await fetch('/api/todos', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ text: todoText, done: false })
        })
        const newTodo = await response.json()
        commit('addTodo', newTodo)
      } catch (error) {
        console.error('Error adding todo:', error)
      }
    }
  }
})

在组件中分发actions:

// Vue 2
export default {
  created() {
    this.$store.dispatch('fetchTodos')
  },
  methods: {
    addTodo() {
      this.$store.dispatch('addTodo', this.newTodoText)
    }
  }
}

// Vue 3
import { useStore } from 'vuex'
import { ref, onMounted } from 'vue'

export default {
  setup() {
    const store = useStore()
    const newTodoText = ref('')
    
    onMounted(() => {
      store.dispatch('fetchTodos')
    })
    
    function addTodo() {
      store.dispatch('addTodo', newTodoText.value)
      newTodoText.value = ''
    }
    
    return { newTodoText, addTodo }
  }
}

2.1.5 Modules

Modules用于将store分割成模块:

// store/modules/counter.js
export default {
  namespaced: true,
  state: {
    count: 0
  },
  getters: {
    doubleCount: state => state.count * 2
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  }
}

// store/modules/todos.js
export default {
  namespaced: true,
  state: {
    list: []
  },
  getters: {
    doneTodos: state => state.list.filter(todo => todo.done)
  },
  mutations: {
    add(state, todo) {
      state.list.push(todo)
    }
  },
  actions: {
    async fetchAll({ commit }) {
      const response = await fetch('/api/todos')
      const todos = await response.json()
      todos.forEach(todo => commit('add', todo))
    }
  }
}

// store/index.js
import { createStore } from 'vuex'
import counter from './modules/counter'
import todos from './modules/todos'

export default createStore({
  modules: {
    counter,
    todos
  }
})

在组件中访问模块:

// Vue 2
export default {
  computed: {
    count() {
      return this.$store.state.counter.count
    },
    doubleCount() {
      return this.$store.getters['counter/doubleCount']
    }
  },
  methods: {
    increment() {
      this.$store.commit('counter/increment')
    },
    incrementAsync() {
      this.$store.dispatch('counter/incrementAsync')
    }
  }
}

// Vue 3
import { computed } from 'vue'
import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()
    
    const count = computed(() => store.state.counter.count)
    const doubleCount = computed(() => store.getters['counter/doubleCount'])
    
    function increment() {
      store.commit('counter/increment')
    }
    
    function incrementAsync() {
      store.dispatch('counter/incrementAsync')
    }
    
    return { count, doubleCount, increment, incrementAsync }
  }
}

2.2 Vuex辅助函数

Vuex提供了一些辅助函数简化代码:

2.2.1 mapState

// Vue 2
import { mapState } from 'vuex'

export default {
  computed: {
    // 映射this.count为store.state.count
    ...mapState({
      count: state => state.count,
      countAlias: 'count',
      countPlusLocalState(state) {
        return state.count + this.localCount
      }
    }),
    
    // 映射this.count为store.state.count
    ...mapState(['count', 'todos'])
  }
}

2.2.2 mapGetters

// Vue 2
import { mapGetters } from 'vuex'

export default {
  computed: {
    // 映射this.doneTodosCount为store.getters.doneTodosCount
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
    ]),
    
    // 使用别名
    ...mapGetters({
      doneCount: 'doneTodosCount'
    })
  }
}

2.2.3 mapMutations

// Vue 2
import { mapMutations } from 'vuex'

export default {
  methods: {
    // 映射this.increment()为this.$store.commit('increment')
    ...mapMutations([
      'increment',
      'incrementBy'
    ]),
    
    // 使用别名
    ...mapMutations({
      add: 'increment'
    })
  }
}

2.2.4 mapActions

// Vue 2
import { mapActions } from 'vuex'

export default {
  methods: {
    // 映射this.fetchTodos()为this.$store.dispatch('fetchTodos')
    ...mapActions([
      'fetchTodos',
      'addTodo'
    ]),
    
    // 使用别名
    ...mapActions({
      fetch: 'fetchTodos'
    })
  }
}

2.2.5 createNamespacedHelpers

// Vue 2
import { createNamespacedHelpers } from 'vuex'

// 创建基于某个命名空间辅助函数
const { mapState, mapActions } = createNamespacedHelpers('counter')

export default {
  computed: {
    // 映射this.count为store.state.counter.count
    ...mapState({
      count: state => state.count
    })
  },
  methods: {
    // 映射this.increment()为this.$store.dispatch('counter/increment')
    ...mapActions([
      'increment',
      'incrementAsync'
    ])
  }
}

2.3 Vuex插件

Vuex支持插件系统,用于扩展功能:

// 简单的日志插件
const myPlugin = store => {
  // 当store初始化后调用
  store.subscribe((mutation, state) => {
    // 每次mutation之后调用
    console.log('mutation type:', mutation.type)
    console.log('mutation payload:', mutation.payload)
    console.log('current state:', state)
  })
}

// 持久化状态插件
const persistStatePlugin = store => {
  // 从localStorage恢复状态
  const savedState = localStorage.getItem('vuex-state')
  if (savedState) {
    store.replaceState(JSON.parse(savedState))
  }
  
  // 订阅状态变更
  store.subscribe((mutation, state) => {
    localStorage.setItem('vuex-state', JSON.stringify(state))
  })
}

// 在store中使用插件
export default createStore({
  // ...
  plugins: [myPlugin, persistStatePlugin]
})

3. Pinia基础

3.1 Pinia介绍

Pinia是Vue官方团队成员开发的新一代状态管理库,被视为Vuex 5的替代品。它提供了更简单的API、完整的TypeScript支持,以及更好的开发体验。

3.2 安装和基本设置

# 安装Pinia
npm install pinia
# 或
yarn add pinia

在Vue应用中注册Pinia:

// main.js (Vue 3)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

3.3 定义Store

在Pinia中,store的定义更加直观:

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

// 第一个参数是store的唯一ID
export const useCounterStore = defineStore('counter', {
  // state是一个返回初始状态的函数
  state: () => ({
    count: 0,
    name: 'Eduardo'
  }),
  
  // getters类似于组件的计算属性
  getters: {
    doubleCount: (state) => state.count * 2,
    // 使用this访问其他getters
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },
  
  // actions包含可以修改状态和执行异步操作的方法
  actions: {
    increment() {
      this.count++
    },
    async fetchData() {
      const response = await fetch('/api/data')
      const data = await response.json()
      this.count = data.count
    }
  }
})

3.4 使用Store

在组件中使用Pinia store:

// Vue 3 组件
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

// 获取store实例
const counterStore = useCounterStore()

// 解构store状态和getters (使用storeToRefs保持响应性)
const { count, name } = storeToRefs(counterStore)
const { doubleCount } = storeToRefs(counterStore)

// 直接调用actions
function handleClick() {
  counterStore.increment()
}

// 异步actions
async function loadData() {
  await counterStore.fetchData()
}
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double count: {{ doubleCount }}</p>
    <p>Name: {{ name }}</p>
    <button @click="handleClick">Increment</button>
    <button @click="loadData">Load Data</button>
  </div>
</template>

3.5 修改状态

Pinia提供了多种修改状态的方式:

3.5.1 直接修改

const store = useCounterStore()

// 直接修改状态
store.count++
store.name = 'Alex'

3.5.2 使用actions

const store = useCounterStore()

// 通过actions修改
store.increment()

3.5.3 使用$patch方法

const store = useCounterStore()

// 一次性修改多个状态
store.$patch({
  count: store.count + 1,
  name: 'Alex'
})

// 使用函数形式的$patch处理复杂状态
store.$patch((state) => {
  state.count++
  state.name = 'Alex'
  // 可以包含更复杂的逻辑
  if (state.items) {
    state.items.push({ id: nanoid(), text: 'New Item' })
  }
})

3.5.4 替换整个状态

const store = useCounterStore()

// 完全替换状态
store.$state = {
  count: 10,
  name: 'Alex'
}

3.6 Pinia插件

Pinia支持插件系统,可以扩展store功能:

// plugins/persistPlugin.js
import { toRaw } from 'vue'

export function persistPlugin({ store }) {
  // 从localStorage恢复状态
  const storedState = localStorage.getItem(`pinia-${store.$id}`)
  
  if (storedState) {
    store.$state = JSON.parse(storedState)
  }
  
  // 监听状态变化并保存
  store.$subscribe((mutation, state) => {
    localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(toRaw(state)))
  })
}

// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { persistPlugin } from './plugins/persistPlugin'
import App from './App.vue'

const pinia = createPinia()
pinia.use(persistPlugin)

const app = createApp(App)
app.use(pinia)
app.mount('#app')

4. Vuex与Pinia的对比

4.1 核心概念对比

特性VuexPinia
状态定义state对象state函数
计算状态gettersgetters
同步修改mutationsactions
异步操作actionsactions
模块化modules + namespaced多个store
TypeScript支持有限完全支持
开发工具Vue DevtoolsVue Devtools

4.2 API风格对比

4.2.1 Store定义

Vuex:

// store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    count: 0
  },
  getters: {
    doubleCount: state => state.count * 2
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  }
})

Pinia:

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

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: state => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    },
    incrementAsync() {
      setTimeout(() => {
        this.increment()
      }, 1000)
    }
  }
})

4.2.2 在组件中使用

Vuex (Vue 2):

import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['count']),
    ...mapGetters(['doubleCount'])
  },
  methods: {
    ...mapMutations(['increment']),
    ...mapActions(['incrementAsync'])
  }
}

Vuex (Vue 3 Composition API):

import { computed } from 'vue'
import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()
    
    const count = computed(() => store.state.count)
    const doubleCount = computed(() => store.getters.doubleCount)
    
    function increment() {
      store.commit('increment')
    }
    
    function incrementAsync() {
      store.dispatch('incrementAsync')
    }
    
    return { count, doubleCount, increment, incrementAsync }
  }
}

Pinia:

import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    const store = useCounterStore()
    
    // 解构时保持响应性
    const { count } = storeToRefs(store)
    const { doubleCount } = storeToRefs(store)
    
    // 直接使用actions
    const { increment, incrementAsync } = store
    
    return { count, doubleCount, increment, incrementAsync }
  }
}

4.3 优缺点比较

4.3.1 Vuex优缺点

优点:

  • 成熟稳定,生态系统丰富
  • 与Vue 2完全兼容
  • 严格的状态管理模式,mutation/action分离
  • 丰富的中间件和插件

缺点:

  • API相对复杂,样板代码较多
  • TypeScript支持有限
  • 模块嵌套时命名空间管理复杂
  • 必须通过mutation修改状态

4.3.2 Pinia优缺点

优点:

  • API简洁,样板代码少
  • 完全支持TypeScript
  • 更好的开发体验和IDE支持
  • 可直接修改状态,无需mutations
  • 更小的包体积
  • 模块化设计更简单,无需嵌套

缺点:

  • 相对较新,生态系统不如Vuex丰富
  • 对Vue 2需要额外适配
  • 缺少严格模式的状态追踪

5. 高级状态管理模式

5.1 组合式Store模式

5.1.1 Vuex中的组合

// store/modules/user.js
export default {
  namespaced: true,
  state: () => ({
    profile: null,
    preferences: {}
  }),
  getters: {
    isLoggedIn: state => !!state.profile
  },
  mutations: {
    setProfile(state, profile) {
      state.profile = profile
    }
  },
  actions: {
    async login({ commit }, credentials) {
      const profile = await authService.login(credentials)
      commit('setProfile', profile)
      return profile
    }
  }
}

// store/modules/cart.js
export default {
  namespaced: true,
  state: () => ({
    items: []
  }),
  getters: {
    totalPrice: state => {
      return state.items.reduce((total, item) => {
        return total + (item.price * item.quantity)
      }, 0)
    }
  },
  mutations: {
    addItem(state, item) {
      state.items.push(item)
    }
  },
  actions: {
    async checkout({ state, commit }) {
      await orderService.createOrder(state.items)
      // 清空购物车
      state.items = []
    }
  }
}

// store/index.js
import { createStore } from 'vuex'
import user from './modules/user'
import cart from './modules/cart'

export default createStore({
  modules: {
    user,
    cart
  }
})

5.1.2 Pinia中的组合

// stores/user.js
import { defineStore } from 'pinia'
import { authService } from '@/services/auth'

export const useUserStore = defineStore('user', {
  state: () => ({
    profile: null,
    preferences: {}
  }),
  getters: {
    isLoggedIn: state => !!state.profile
  },
  actions: {
    async login(credentials) {
      const profile = await authService.login(credentials)
      this.profile = profile
      return profile
    },
    logout() {
      this.profile = null
    }
  }
})

// stores/cart.js
import { defineStore } from 'pinia'
import { orderService } from '@/services/order'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  getters: {
    totalPrice: state => {
      return state.items.reduce((total, item) => {
        return total + (item.price * item.quantity)
      }, 0)
    }
  },
  actions: {
    addItem(item) {
      this.items.push(item)
    },
    async checkout() {
      await orderService.createOrder(this.items)
      this.items = []
    }
  }
})

// 在组件中组合使用多个store
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'

export default {
  setup() {
    const userStore = useUserStore()
    const cartStore = useCartStore()
    
    async function checkoutAsUser() {
      if (!userStore.isLoggedIn) {
        await userStore.login()
      }
      await cartStore.checkout()
    }
    
    return { userStore, cartStore, checkoutAsUser }
  }
}

5.2 持久化状态

5.2.1 Vuex持久化

使用vuex-persistedstate插件:

// store/index.js
import { createStore } from 'vuex'
import createPersistedState from 'vuex-persistedstate'

export default createStore({
  // ...store配置
  plugins: [
    createPersistedState({
      key: 'vuex-state',
      paths: ['user.profile', 'cart.items'] // 只持久化特定路径
    })
  ]
})

5.2.2 Pinia持久化

使用pinia-plugin-persistedstate插件:

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

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

const app = createApp(App)
app.use(pinia)
app.mount('#app')

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

export const useUserStore = defineStore('user', {
  state: () => ({
    profile: null
  }),
  // 配置持久化
  persist: {
    key: 'user-store',
    storage: localStorage,
    paths: ['profile']
  },
  // ...其他配置
})

5.3 状态重置

5.3.1 Vuex状态重置

// store/index.js
export default createStore({
  // ...
  mutations: {
    // 添加重置状态的mutation
    resetState(state) {
      Object.assign(state, initialState)
    }
  },
  actions: {
    resetStore({ commit }) {
      commit('resetState')
    }
  }
})

// 在组件中使用
methods: {
  logout() {
    this.$store.dispatch('resetStore')
  }
}

5.3.2 Pinia状态重置

Pinia提供了内置的$reset方法:

// 在组件中使用
const store = useCounterStore()

// 重置store到初始状态
function resetStore() {
  store.$reset()
}

5.4 状态订阅

5.4.1 Vuex状态订阅

// 订阅mutation
store.subscribe((mutation, state) => {
  console.log('mutation type:', mutation.type)
  console.log('mutation payload:', mutation.payload)
  console.log('current state:', state)
})

// 订阅action
store.subscribeAction({
  before: (action, state) => {
    console.log(`before action ${action.type}`)
  },
  after: (action, state) => {
    console.log(`after action ${action.type}`)
  }
})

5.4.2 Pinia状态订阅

const store = useCounterStore()

// 订阅状态变化
const unsubscribe = store.$subscribe((mutation, state) => {
  // 每次状态变化时触发
  console.log('mutation type:', mutation.type)
  console.log('mutation payload:', mutation.payload)
  console.log('current state:', state)
}, { detached: false }) // detached: true 会在组件卸载后保持订阅

// 订阅action
store.$onAction(({
  name, // action名称
  store, // store实例
  args, // 传递给action的参数数组
  after, // 在action返回或解决后的钩子
  onError // action抛出或拒绝的钩子
}) => {
  console.log(`Start "${name}" with params [${args.join(', ')}]`)
  
  after((result) => {
    console.log(`Finished "${name}" with result: ${result}`)
  })
  
  onError((error) => {
    console.error(`Failed "${name}" with error: ${error}`)
  })
})

结语
感谢您的阅读!期待您的一键三连!欢迎指正!

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Guiat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值