Skip to content

Vue3状态管理工具(Vuex与Pinia)

以下内容基于B站教学视频总结,鉴于推荐先使用Vuex再讲解Pinia,所以笔记先从Vuex写起:

Vuex 是什么?(官方文档) | Vuex (vuejs.org)

安装

npm install vuex@next --save

--save 选项可以将对应的包加入到package.json的依赖中,所以当其他用户部署时,可以自动安装依赖,并且可以在package.json中,自动读取使用的版本号。

作为插件引入

首先创建/store/index.js文件

js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router/index'
import store from './store/index'
// 这里把 store 作为插件注入,后续组件中才能 useStore()
// 这一步是全局注册
// 少了这一步就拿不到仓库实例
createApp(App).use(router).use(store).mount('#app')

创建完成后,在Vue+vite中,需要在main.js文件中,将vuex引入

编写Vuex主要代码

/store/index.js中,需要实现createStore并抛出。

js
import { createStore } from "vuex";
const store = createStore({
    //实例化仓库对象
    state() {
        return {
            // 这里是全局共享状态,多个组件都可以读取
            todoListArray : ['vue', 'vite', 'vuex']
        }
    },
    // 仓库中的方法
    mutations: {
        addTodo (state, todo) {
            // 约定 mutation 里做同步修改,便于追踪状态变更来源
            // state 是当前仓库状态快照
            // todo 是 commit 传入的 payload
            state.todoListArray.push(todo)
          },
    }
})
export default store

或者也可以将mutations等方法,编写为mutations.js再引入。 详细业务逻辑可以参考:vuex/examples/composition/todomvc/store/mutations.js at main · vuejs/vuex (github.com)

仓库中变量的读取

读取变量相对简单,只需要从"vuex"中import {useStore} from "vuex" ,并使用计算属性,读取对应的变量即可:

vue
<script setup>
import {useStore} from "vuex"
import { computed } from 'vue'
const store = useStore();
const lists = computed(() => {
  // 使用 computed 包一层,模板消费时保持响应式
  // store.state 变化后
  // lists 会自动重算
  return store.state.todoListArray
})
</script>

更新仓库中的变量

/store/index.js文件中,我们曾创建过相关方法:

js
    // 仓库中的方法
    mutations: {
        addTodo (state, todo) {
            state.todoListArray.push(todo)
          },
        ......
    }

那么更新时,我们也需要调用方法去更新,而不是直接修改仓库里的变量。 调用的方法为

js
store.commit('addTodo', valRef.value)   // 同步 对应mutations方法
store.dispatch('addTodo', valRef.value)  // 异步 对应actions方法

传参中,valRef.value变量,对应着仓库mutationsaddTodo方法中第二个传参,即todo。 完整代码为:

vue
<script setup>  
import { useStore } from 'vuex'  
import {ref,computed} from "vue"  
const valRef = ref("")  
const store = useStore();  
const lists = computed(()=>{  
  // 读取 Vuex 全局状态,页面会自动跟随更新
    return store.state.todoListArray;  
})  
function add(e) {  
    if (valRef.value === "")  
        return;  
    // 通过vuex的相关方法传出参数  
    store.commit('addTodo', valRef.value)   // 同步  
    // store.dispatch('addTodo', valRef.value)  // 异步  
  // 不建议直接写 store.state.todoListArray.push(...),
  // 会绕开 mutation 约定,后续排查变更来源会很痛苦
  // commit 会进入 mutation
  // devtools 也能记录这次变更
    valRef.value = "" // 输入input框置空  
}  
</script>  
  
<template>  
    <div>  
        <input type="text" v-model="valRef">  
        <button @click="add">提交</button>  
    </div>  
</template>

更多代码样例可参考:vuex/examples/composition/todomvc/components/TodoItem.vue at main · vuejs/vuex (github.com)

补充,选项式的编写方式可以参考:回顾一下Vue组件通讯的三种方式 - 掘金 (juejin.cn)

一个更贴近业务的 Vuex 场景

例如后台管理系统里。 筛选条件在列表页、导出弹窗、统计面板都要复用。 这时可以放进 Vuex 的 state。 每次改筛选只通过 mutation。 这样页面联动会更稳定,状态来源也更清楚。

Pinia

Pinia是Vuex状态管理工具的替代品 优势:

  1. 提供更加简单的api(去掉了mutation)
  2. 提供了组合式风格的api
  3. 去掉了modules的概念, 每一个store都是一个独立的模块
  4. 搭配TypeScript一起使用提供可靠的类型推断

从这里开始可以把理解切到另一个层面。 如果说 Vuex 更强调“流程规范”。 那么 Pinia 更强调“开发体验和组合式风格”。 两者都能做状态管理,差异主要在写法和维护习惯。

使用npm install pinia --save即可安装

Vue3+vite中引入

官方文档: 开始 | Pinia (vuejs.org)

js
// 首先, 创建一个pinia实例, 并将其传递给应用: 
import {createPinia} from "pinia";  

const pinia = createPinia()  

// 和 Vuex 一样,仍然通过 app.use() 注入
createApp(App).use(pinia).mount('#app')

创建Pinia的Store

创建目录store,并创建一个js文件

js
import {defineStore} from "pinia";  

export const useTodoListArrayStore = defineStore('todoList', {  
  // id 要保证唯一,后续 devtools 会用它识别 store
    state : ()=> ({  
      // state 写成函数
      // 每次实例化都会拿到新对象
        todoListArray : ['vue', 'vite', 'pinia']  
    }),  
    actions: {  
        addTodo(val) {  
      // Pinia 中 actions 既能写同步,也能写异步
      // this 指向当前 store 实例
            this.todoListArray.push(val);  
        }  
    }  
})

或者使用更加组合式api风格的写法 :

js
// 注意export抛出结果
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  // 组合式写法里,getter 通常直接用 computed
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    // 直接改 ref
    // Pinia 会追踪到这次更新
    count.value++
  }

  // 哪些状态和方法要对外暴露,就 return 哪些
  return { count, doubleCount, increment }
})

使用Pinia

pinia的使用要比vuex来着直观一些,鉴于我们已经在js文件中导出来我们的对象,我们可以在其他文件中,直接引入该对象,比如上文中的useTodoListArrayStore

vue
<script setup>  
  
import {useTodoListArrayStore} from "../../store/piniaTodoList.js";  
// 这个调用拿到的是当前 store 实例
const piniaStore = useTodoListArrayStore();  
  
</script>

// 而使用时,与vuex不同,这里不需要computed属性,直接使用piniaStore对象即可,比如
<template>  
  <!-- 这里直接访问 store 状态,不需要再包 computed -->
      <li v-for="(item,index) in piniaStore.todoListArray" :key="index">{{ item }}
      </li>
</template>

而写入或者修改对象,也十分容易,相同的,我们首先在其他文件中引入

js
import {useTodoListArrayStore} from "../../store/piniaTodoList.js";  
const piniaStore = useTodoListArrayStore();

引入完成后,可以直接通过piniaStore调用到上文中编写的actions相关方法 例如:

js
// 通过pinia的方式传参  
// 和调用普通对象方法几乎一致,心智负担更低
// 这里不需要 commit
piniaStore.addTodo(valRef.value)

一个更贴近业务的 Pinia 场景

例如电商首页。 用户信息、购物车数量、主题色偏好会在多个区域复用。 可以拆成多个 Pinia store。 例如 useUserStoreuseCartStoreuseThemeStore。 每个 store 只关注自己的职责,目录结构会更清晰。

$patch 的使用

$patch() 是 Pinia 中用来批量更新状态的方法。它允许传递一个对象,这个对象的属性会合并到 store 的现有状态中,从而更新对应的状态值。可以选择传递部分属性来更新部分状态,而不需要替换整个状态对象。

js
// 假设有一个 Pinia store
const counter = useCounterStore();

// 使用 $patch 更新 count 属性
// 适合一次改多个字段,避免多次触发更新逻辑
// 对象形式会做浅合并
counter.$patch({ count: counter.count + 1 });

如果是一次更新多个值,也可以这样写。

js
counter.$patch({
  count: counter.count + 1,
  lastUpdateTime: Date.now(),
  source: 'button-click'
})

getters实现

使用computed() 方法, 返回一个函数的返回结果

js
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)

  // 类似getter
  // count 改变时,doubleCount 会自动重新计算
  const doubleCount = computed(() => count.value * 2)
  
  function increment() {
    count.value++
  }

  // 注意需要return回去
  return { count, doubleCount, increment }
})
js
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
})

异步action

js
const list = ref([])
const getList = async () => {
	// action 内可以直接写异步请求
	// 失败时建议补 try/catch 和错误状态
  // 成功后直接写回 store 状态
	const res = await axios.get(API_URL)
	list.value = res.data.data.channels
}
return {list, getList}

更完整一点的写法通常是这样。

js
const list = ref([])
const loading = ref(false)
const errorMsg = ref('')

const getList = async () => {
  loading.value = true
  errorMsg.value = ''
  try {
    // 进入请求前先清空旧错误
    const res = await axios.get(API_URL)
    // 成功后写入列表
    list.value = res.data.data.channels
  } catch (err) {
    // 失败后只更新错误态
    errorMsg.value = '列表加载失败'
  } finally {
    // 无论成功失败都关闭 loading
    loading.value = false
  }
}

return { list, loading, errorMsg, getList }

storeToRefs

使用storeToRefs()函数可以辅助保持数据(state+gatter)的响应式解构 如果使用如下代码对counterStroe进行解构赋值, 将导致响应式丢失

这里有一个容易踩坑的点。 storeToRefs 只会处理 state 和 getters。 不会把 actions 转成 ref。

  • 方法可以直接通过对counterStore进行结构而获得
  • const {increment} = counterStore
js
const {count, doubleCount} = counterStore

如果想要实现响应式的解构, 则可以:

js
import {storeToRefs} from 'pinia'
// 这里返回的是 ref
// 解构后仍保持响应式连接
const {count, doubleCount} = storeToRefs(counterStore)

Vuex 的优势在于流程清晰和历史项目沉淀。 Pinia 的优势在于写法直观和组合式契合度。

更多关于Pinia的内容,可以参考: 简介 | Pinia (vuejs.org)Pinia🍍还不会用?请看这个TodoList小Demo! - 掘金 (juejin.cn)