前端 Vue 专栏 10:Pinia 与前端状态管理

2 阅读14分钟

前端 Vue 专栏 10:Vuex、Pinia 与前端状态管理

前言

前面的文章已经介绍了组件通信和 Vue Router。随着页面增多,我们还会遇到一个问题:

多个组件和页面需要使用同一份数据时,这份数据应该放在哪里?

例如登录成功后,下面这些地方都需要读取当前用户:

顶部导航栏 → 显示用户名
个人中心   → 显示用户资料
权限菜单   → 判断用户角色
订单页面   → 判断是否登录

如果每个组件都各自请求和保存用户信息,容易产生重复请求和数据不一致;如果一直通过 props 逐层传递,组件关系又会变得很复杂。

这时可以使用全局状态管理。

Vue 项目中最常见的两种状态管理方案是:

Vuex  → Vue 早期项目和存量项目中常见
Pinia → 当前 Vue 新项目通常优先采用

这一篇将围绕普通前端开发和秋招高频问题展开:

什么是全局状态;
什么时候需要状态管理;
Vuex 的 state、getters、mutations、actions;
Vuex 为什么区分 mutation 和 action;
Vuex modules 有什么用;
Pinia 的 state、getters、actions;
storeToRefs 为什么能够保持响应性;
Vuex 和 Pinia 有什么区别;
实际项目应该怎样选择。

一、什么是状态管理

状态是会影响页面展示或业务行为的数据:

const user = ref(null)
const cartItems = ref([])
const theme = ref('light')

它们分别决定:

当前登录的是谁;
购物车有哪些商品;
页面使用浅色还是深色主题。

状态管理主要解决四个问题:

状态保存在哪里;
哪些组件可以读取;
通过什么方式修改;
状态变化后哪些页面需要更新。

Vuex 和 Pinia 都会把 store 中的 state 接入 Vue 响应式系统:

store 状态发生变化
        ↓
依赖它的 getter 重新计算
        ↓
使用它的组件响应更新

它们的主要区别不在于“能不能响应式”,而在于状态和操作的组织方式。


二、什么时候需要全局状态

适合放入全局 store 的数据通常具有以下特点:

多个无直接父子关系的组件需要使用;
切换路由后仍然需要保留;
需要在多个位置保持一致;
围绕它存在统一的业务操作。

常见例子:

当前用户和登录状态;
购物车;
全局主题;
权限信息;
需要跨页面保存的流程数据。

下面这些状态通常不需要放入全局 store:

当前组件的弹窗是否打开;
输入框正在输入的临时内容;
某个局部菜单是否展开;
只在当前页面使用的简单请求结果。

状态管理不是越多越好。可以按照下面的顺序判断:

单个组件使用       → ref / reactive
明确的父子通信     → props / emit
局部组件树共享     → provide / inject
多个页面全局共享   → Vuex / Pinia

第一部分:Vuex

三、Vuex 的整体结构

Vuex 是集中式状态管理库,使用一个 store 保存应用级状态。

它有五个核心概念:

概念作用
state保存原始状态
getters根据 state 计算派生状态
mutations同步修改 state
actions组织业务和异步操作
modules按业务拆分大型 store

最重要的数据流是:

同步修改:
组件 → commit mutation → 修改 state → 页面更新

异步业务:
组件 → dispatch action → commit mutation → 修改 state → 页面更新

其中两个词需要先记住:

commit   → 提交 mutation
dispatch → 派发 action

四、创建和注册 Vuex Store

Vue 3 项目通常使用 Vuex 4:

npm install vuex@4

创建 store:

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

const store = createStore({
  state() {
    return {
      count: 0
    }
  },

  getters: {
    doubleCount(state) {
      return state.count * 2
    }
  },

  mutations: {
    increment(state) {
      state.count++
    }
  },

  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  }
})

export default store

在入口文件中注册:

// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'

const app = createApp(App)

app.use(store)
app.mount('#app')

注册后,应用中的组件会使用同一个 Vuex store。


五、Vuex state 和 getters

1. state

state 用来保存原始状态:

state() {
  return {
    count: 0,
    user: null,
    cartItems: []
  }
}

读取方式:

store.state.count
store.state.user
store.state.cartItems

Vuex state 是响应式的。state 变化后,使用它的组件会更新。

2. getters

getters 类似 store 中的 computed

getters: {
  doubleCount(state) {
    return state.count * 2
  },

  isLoggedIn(state) {
    return Boolean(state.user)
  }
}

读取方式:

store.getters.doubleCount
store.getters.isLoggedIn

普通 getter 作为属性读取,不需要写成函数调用:

store.getters.doubleCount

而不是:

store.getters.doubleCount()

如果一个值能够完全由 state 计算出来,就优先考虑 getter,避免保存两份可能不同步的状态。


六、Vuex mutations:同步修改状态

在 Vuex 的标准数据流中,mutation 负责真正修改 state:

mutations: {
  increment(state) {
    state.count++
  }
}

通过 commit 提交:

store.commit('increment')

执行过程:

commit('increment')
        ↓
找到 increment mutation
        ↓
执行 state.count++
        ↓
组件更新

mutation 接收参数

mutations: {
  incrementBy(state, amount) {
    state.count += amount
  }
}

提交时传递 payload:

store.commit('incrementBy', 5)

也可以传递对象:

mutations: {
  setUser(state, user) {
    state.user = user
  }
}
store.commit('setUser', {
  id: 1001,
  name: '小明'
})

mutation 为什么要求同步

Vuex 希望每次状态变化都能够被开发工具准确记录:

mutation 执行前的 state
        ↓
同步执行 mutation
        ↓
mutation 执行后的 state

如果在 mutation 中放入异步请求,状态具体在什么时候改变就难以对应到本次 mutation。

因此不要这样写:

mutations: {
  async fetchUser(state) {
    const response = await fetch('/api/user')
    state.user = await response.json()
  }
}

异步逻辑应该放在 action 中。


七、Vuex actions:处理业务和异步操作

action 可以执行同步或异步业务,但按照 Vuex 的设计,它通过提交 mutation 修改状态。

actions: {
  incrementAsync(context) {
    setTimeout(() => {
      context.commit('increment')
    }, 1000)
  }
}

action 的第一个参数是 context:

context.state    → 读取 state
context.getters  → 读取 getters
context.commit   → 提交 mutation
context.dispatch → 调用其他 action

通常直接解构需要的内容:

actions: {
  incrementAsync({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

通过 dispatch 调用:

store.dispatch('incrementAsync')

一个真实请求例子

const store = createStore({
  state() {
    return {
      user: null,
      loading: false,
      error: null
    }
  },

  mutations: {
    setLoading(state, loading) {
      state.loading = loading
    },

    setUser(state, user) {
      state.user = user
    },

    setError(state, error) {
      state.error = error
    }
  },

  actions: {
    async fetchUser({ commit }, userId) {
      commit('setLoading', true)
      commit('setError', null)

      try {
        const response = await fetch(
          `/api/users/${userId}`
        )

        if (!response.ok) {
          throw new Error('获取用户失败')
        }

        const user = await response.json()
        commit('setUser', user)

        return user
      } catch (error) {
        commit('setError', error.message)
        throw error
      } finally {
        commit('setLoading', false)
      }
    }
  }
})

组件调用:

await store.dispatch('fetchUser', 1001)

流程是:

dispatch fetchUser
        ↓
action 发起请求
        ↓
action commit setUser
        ↓
mutation 修改 state.user
        ↓
组件响应更新

同步操作必须经过 action 吗

不一定。

简单同步修改可以直接:

store.commit('increment')

异步请求或复杂业务再使用:

store.dispatch('fetchUser')

不要为了形式统一,给每一个简单 mutation 都套一层没有业务逻辑的 action。


八、组件如何使用 Vuex

组合式 API 中使用 useStore()

<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'

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')
}
</script>

<template>
  <p>数量:{{ count }}</p>
  <p>两倍:{{ doubleCount }}</p>

  <button @click="increment">
    同步增加
  </button>

  <button @click="incrementAsync">
    延迟增加
  </button>
</template>

需要记住四种访问方式:

操作Vuex 写法
读取 statestore.state.count
读取 getterstore.getters.doubleCount
提交 mutationstore.commit('increment')
派发 actionstore.dispatch('incrementAsync')

存量项目的选项式 API 中还经常出现:

mapState
mapGetters
mapMutations
mapActions

它们只是把 store 中的状态和方法映射到组件中。秋招需要能看懂,但不必把所有写法都背下来。


九、Vuex modules 有什么用

Vuex 使用单一 store。业务变多以后,根 store 可能非常庞大:

store
├── user
├── cart
├── product
├── order
└── theme

modules 用来按照业务拆分:

const userModule = {
  namespaced: true,

  state: () => ({
    user: null
  }),

  getters: {
    isLoggedIn(state) {
      return Boolean(state.user)
    }
  },

  mutations: {
    setUser(state, user) {
      state.user = user
    }
  },

  actions: {
    async fetchUser({ commit }) {
      const response = await fetch('/api/user')
      const user = await response.json()

      commit('setUser', user)
    }
  }
}

注册模块:

const store = createStore({
  modules: {
    user: userModule
  }
})

读取模块状态:

store.state.user.user

开启:

namespaced: true

以后,提交和派发时带上模块名:

store.commit('user/setUser', user)
store.dispatch('user/fetchUser')

可以这样理解:

user/setUser
│    └── mutation 名称
└─────── module 命名空间

对于普通前端开发,掌握模块拆分和 namespaced: true 就足够了,不需要深入动态注册和多层嵌套模块。


第二部分:Pinia

十、Pinia 的整体结构

Pinia 也是 Vue 的状态管理库,但它简化了 Vuex 的数据流。

Pinia 主要有三个概念:

概念作用
state保存原始状态
getters计算派生状态
actions封装同步或异步业务,并可直接修改 state

最重要的流程是:

组件调用 action
        ↓
action 执行业务并修改 state
        ↓
getters 重新计算
        ↓
组件响应更新

与 Vuex 相比,Pinia 没有单独的 mutations:

Vuex:action → commit mutation → state
Pinia:action → state

十一、创建和注册 Pinia

安装:

npm install pinia

注册:

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

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

Pinia 通常按照业务拆分多个 store:

src/stores/
├── user.js
├── cart.js
└── theme.js

每个 store 都有独立 id:

defineStore('user', {})
defineStore('cart', {})
defineStore('theme', {})

不同 store 的 id 不应该重复。


十二、定义 Pinia Option Store

// src/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++
    },

    async incrementAsync() {
      await new Promise(resolve => {
        setTimeout(resolve, 1000)
      })

      this.count++
    }
  }
})

对应关系:

state:count
getter:doubleCount
action:increment、incrementAsync

组件中先调用:

const counterStore = useCounterStore()

得到 store 实例后,可以:

counterStore.count
counterStore.doubleCount
counterStore.increment()
counterStore.incrementAsync()

十三、Pinia state、getters 和 actions

1. state

Option Store 中直接声明普通初始值即可:

state: () => ({
  count: 0,
  user: null,
  items: []
})

Pinia 会将它们纳入 Vue 响应式系统,不需要写成:

state: () => ({
  count: ref(0)
})

Pinia 允许直接修改 state:

counterStore.count++

不过涉及请求、校验或多步修改的业务,通常封装为 action 更清晰:

cartStore.addItem(product)

2. getters

getters: {
  totalCount: state => {
    return state.items.reduce(
      (sum, item) => sum + item.quantity,
      0
    )
  }
}

像属性一样读取:

cartStore.totalCount

getters 适合保存由 state 推导出的计算规则,不适合执行网络请求和产生副作用。

3. actions

Pinia action 中同步和异步逻辑都可以写:

actions: {
  increment() {
    this.count++
  },

  async fetchUser() {
    const response = await fetch('/api/user')
    this.user = await response.json()
  }
}

与 Vuex 不同,Pinia action 可以直接修改 state,不需要再 commit mutation。

Option Store 的 action 如果使用 this,不要使用箭头函数:

actions: {
  // 错误:箭头函数没有自己的 this
  increment: () => {
    this.count++
  }
}

应该写成普通方法:

actions: {
  increment() {
    this.count++
  }
}

十四、组件如何使用 Pinia

<script setup>
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()
</script>

<template>
  <p>数量:{{ counterStore.count }}</p>
  <p>两倍:{{ counterStore.doubleCount }}</p>

  <button @click="counterStore.increment">
    增加
  </button>
</template>

直接使用:

counterStore.count
counterStore.doubleCount

会保持响应性。

但是不要直接解构 state 和 getter:

const {
  count,
  doubleCount
} = counterStore

这可能会取得解构时的普通值,丢失与 store 的响应式联系。

需要解构时使用 storeToRefs()

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

const counterStore = useCounterStore()

const {
  count,
  doubleCount
} = storeToRefs(counterStore)

const {
  increment,
  incrementAsync
} = counterStore
</script>

规则是:

不解构 state/getters → 直接使用 store.xxx
解构 state/getters   → 使用 storeToRefs(store)
解构 actions         → 可以直接从 store 解构

actions 是已经由 Pinia 绑定到 store 的方法,本身不是需要响应式追踪的数据,因此不需要通过 storeToRefs() 处理。


十五、Pinia 异步请求的完整例子

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

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    loading: false,
    error: null
  }),

  getters: {
    isLoggedIn: state => Boolean(state.user),

    displayName: state => {
      return state.user?.name || '未登录'
    }
  },

  actions: {
    async fetchUser(userId) {
      this.loading = true
      this.error = null

      try {
        const response = await fetch(
          `/api/users/${userId}`
        )

        if (!response.ok) {
          throw new Error('获取用户失败')
        }

        this.user = await response.json()

        return this.user
      } catch (error) {
        this.error = error.message
        throw error
      } finally {
        this.loading = false
      }
    },

    logout() {
      this.user = null
    }
  }
})

组件:

<script setup>
import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

const {
  user,
  loading,
  error,
  displayName
} = storeToRefs(userStore)

onMounted(() => {
  userStore.fetchUser(1001)
})
</script>

<template>
  <p v-if="loading">加载中...</p>
  <p v-else-if="error">{{ error }}</p>
  <p v-else-if="user">你好,{{ displayName }}</p>
</template>

流程比 Vuex 少了一层 mutation:

组件调用 fetchUser
        ↓
action 发起请求
        ↓
action 直接修改 user、loading、error
        ↓
组件响应更新

十六、Pinia Setup Store

Pinia 还支持接近 Composition API 的写法:

import { computed, ref } from 'vue'
import { defineStore } from 'pinia'

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

    const doubleCount = computed(
      () => count.value * 2
    )

    function increment() {
      count.value++
    }

    return {
      count,
      doubleCount,
      increment
    }
  }
)

对应关系:

ref / reactive → state
computed       → getters
function       → actions

两种形式的选择:

Option Store → state、getters、actions 分区明确
Setup Store  → 接近 Composition API,组织方式更自由

普通业务使用哪一种都可以。学习阶段应能看懂两种形式,但不需要为了显得高级而全部改为 Setup Store。


十七、状态持久化只需要掌握什么

Vuex 和 Pinia 默认保存的是 JavaScript 内存状态。

刷新页面时:

旧页面被销毁
    ↓
Vue 应用重新创建
    ↓
store 恢复初始状态

因此状态管理不等于持久化。

如果主题等数据需要刷新后保留,可以配合 localStorage:

const theme = localStorage.getItem('theme') || 'light'

修改时同步保存:

localStorage.setItem('theme', newTheme)

实际项目也可以使用持久化插件,但普通前端首先要掌握三个原则:

只保存确实需要跨刷新的字段;
loading、error 等临时状态不需要保存;
不要无差别把敏感信息写入 localStorage。

登录是否有效和用户是否有权限,最终仍然必须由后端校验,不能只相信前端 store 中的布尔值。


第三部分:Vuex 与 Pinia 对比

十八、同一个计数器的写法对比

Vuex

const store = createStore({
  state() {
    return {
      count: 0
    }
  },

  getters: {
    doubleCount(state) {
      return state.count * 2
    }
  },

  mutations: {
    increment(state) {
      state.count++
    }
  },

  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  }
})

调用:

store.state.count
store.getters.doubleCount
store.commit('increment')
store.dispatch('incrementAsync')

Pinia

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),

  getters: {
    doubleCount: state => state.count * 2
  },

  actions: {
    increment() {
      this.count++
    },

    async incrementAsync() {
      await delay(1000)
      this.count++
    }
  }
})

调用:

const counterStore = useCounterStore()

counterStore.count
counterStore.doubleCount
counterStore.increment()
counterStore.incrementAsync()

这组对比最直观地体现了两者区别:

Vuex 使用 commit 和 dispatch
Pinia 直接访问属性和调用方法

十九、Vuex 和 Pinia 核心区别

对比项Vuex 4Pinia
状态statestate
派生状态gettersgetters
同步修改commit mutation直接修改或调用 action
异步业务dispatch action直接调用 action
mutation有,要求同步没有单独的 mutation
模块组织单一 store 加 modules多个独立 store
命名空间modules 常配置 namespacedstore id 天然区分
TypeScript能支持,但类型写法相对繁琐类型推导通常更自然
组合式 API可以使用使用体验更直接
常见项目存量 Vue 项目新 Vue 项目

1. 数据流不同

Vuex:

dispatch action
    ↓
commit mutation
    ↓
修改 state

Pinia:

调用 action
    ↓
直接修改 state

2. 模块化方式不同

Vuex:

一个根 store
    ↓
拆分 user、cart 等 modules

Pinia:

useUserStore
useCartStore
useThemeStore

每个 store 本身就是独立业务单元。

3. 调用方式不同

Vuex 通过字符串名称:

store.commit('user/setUser', user)
store.dispatch('user/fetchUser')

Pinia 直接调用方法:

userStore.setUser(user)
userStore.fetchUser()

直接方法调用通常更容易获得编辑器跳转、重构和类型提示。

4. Vuex 并不是不能用了

Vuex 仍然可以正常用于 Vue 项目,大量已有项目也使用 Vuex。只是对于新项目,Pinia 通常具有更简洁的数据流和更自然的组合式 API、TypeScript 使用体验。


二十、项目中应该怎样选择

新项目

通常优先选择 Pinia:

API 更少;
不需要 mutations;
多个 store 的组织方式更直接;
组合式 API 使用自然;
TypeScript 类型推导较好。

已经使用 Vuex 的项目

不需要只因为 Pinia 更新就立即重写整个状态层。

应该考虑:

现有代码是否稳定;
迁移能解决什么真实问题;
测试是否完善;
改造成本和风险是否合理。

维护老项目时,应先理解现有 Vuex 数据流:

state
getters
mutations
actions
modules

新功能是否逐步迁移,再根据团队计划决定。

小型页面

如果只有少量局部状态,可能根本不需要 Vuex 或 Pinia:

const visible = ref(false)
const list = ref([])

不要因为项目使用 Vue,就默认安装一个全局状态库。


二十一、常见误区

1. 所有状态都放入 Store

局部弹窗、输入内容等状态应该留在组件中。

2. Vuex action 直接修改 state

按照 Vuex 的标准数据流,action 应该提交 mutation:

actions: {
  updateUser({ commit }, user) {
    commit('setUser', user)
  }
}

3. 在 Vuex mutation 中请求接口

mutation 应保持同步,异步请求放在 action 中。

4. 混淆 commit 和 dispatch

commit mutation
dispatch action

5. Pinia action 不能异步

Pinia action 同步、异步都可以,并且可以直接修改 state。

6. 直接解构 Pinia state

const { count } = counterStore

可能丢失响应性。需要解构时使用:

const { count } = storeToRefs(counterStore)

7. 认为 Store 会自动持久化

Vuex 和 Pinia 默认都是内存状态,刷新后会重新初始化。

8. 使用 Store 代替后端权限校验

前端状态可以被修改,后端必须独立验证身份和权限。


二十二、状态管理高频面试题

1. 什么是 Vuex

可以这样回答:

Vuex 是 Vue 的集中式状态管理库,使用单一状态树保存应用级响应式状态,核心包括 state、getters、mutations、actions 和 modules。

2. Vuex 中 mutation 和 action 有什么区别

可以这样回答:

mutation 通过 commit 提交,负责同步修改 state;action 通过 dispatch 调用,可以执行异步业务,并通过 commit mutation 修改状态。这样便于开发工具准确追踪每次状态变化。

3. Vuex 为什么要求 mutation 同步

可以这样回答:

Vuex 需要记录每次 mutation 执行前后的状态。如果 mutation 内包含异步逻辑,实际状态变化时间无法和本次提交准确对应,不利于调试和状态追踪,所以异步逻辑应放进 action。

4. Vuex modules 有什么用

可以这样回答:

Vuex 使用单一 store,项目变大后可以通过 modules 按用户、购物车等业务拆分 state、getters、mutations 和 actions。通常配置 namespaced,避免不同模块名称冲突。

5. 什么是 Pinia

可以这样回答:

Pinia 是当前 Vue 新项目常用的状态管理库,以多个独立 store 组织业务状态,每个 store 主要包含 state、getters 和 actions。

6. Pinia state 是响应式的吗

可以这样回答:

是。Option Store 中 state 返回的普通对象会被 Pinia 纳入 Vue 响应式系统;Setup Store 则通常使用 ref 或 reactive 定义状态。

7. Pinia action 能同时处理同步和异步吗

可以这样回答:

可以。Pinia 不再单独区分 mutation,action 可以执行同步或异步业务,并直接修改当前 store 的 state。

8. storeToRefs 有什么用

可以这样回答:

直接解构 Pinia store 的 state 和 getters 可能失去响应性。storeToRefs 会把它们转换成保持响应关联的 refs;actions 是绑定好的方法,可以直接解构。

9. Vuex 和 Pinia 的主要区别

可以这样回答:

Vuex 通常使用 dispatch action、commit mutation 的数据流,并通过 modules 拆分单一 store;Pinia 没有 mutations,actions 可以直接修改 state,并以多个独立 store 组织业务,API 和 TypeScript 使用体验更简洁。

10. 什么状态应该放入 Store

可以这样回答:

多个组件或页面共同使用、需要跨路由保持一致,并具有统一业务操作的状态适合放入 store。只属于单个组件的临时交互状态通常留在组件内部。

11. Store 中的数据刷新后还存在吗

可以这样回答:

默认不存在。Vuex 和 Pinia 状态保存在当前页面的 JavaScript 内存中,刷新会重新创建应用和 store。需要保留时应单独设计浏览器或服务端持久化方案。

12. 新项目应该选择 Vuex 还是 Pinia

可以这样回答:

新 Vue 项目通常优先选择 Pinia,因为数据流更简洁,模块组织和类型推导更自然;已有 Vuex 项目不必盲目重写,应根据维护成本和实际收益决定是否迁移。


二十三、本文知识地图

1. 状态范围

局部状态       → ref / reactive
父子通信       → props / emit
应用级共享状态 → Vuex / Pinia

2. Vuex

state      → 原始状态
getters    → 派生状态
mutations  → 同步修改状态
actions    → 业务与异步操作
modules    → 拆分大型 store
commit   → mutation
dispatch → action

3. Pinia

state    → 原始状态
getters  → 派生状态
actions  → 同步、异步业务并修改状态

4. 最核心区别

Vuex:
action → mutation → state

Pinia:
action → state

5. Pinia 解构规则

state / getters → storeToRefs
actions         → 直接解构

6. 秋招优先级

第一优先:什么状态需要全局管理
第二优先:Vuex 五个核心概念和数据流
第三优先:Pinia 三个核心概念和组件使用
第四优先:mutation、action、commit、dispatch
第五优先:storeToRefs 和响应性
第六优先:Vuex modules 与两种方案对比
了解即可:持久化插件、动态模块、SSR 和内部源码

二十四、总结

Vuex 和 Pinia 都用于管理跨组件、跨页面共享的响应式状态,但它们的数据流不同。

Vuex 的核心是:

state 保存状态;
getters 计算派生状态;
mutation 同步修改 state;
action 处理业务和异步逻辑;
modules 拆分大型 store。

异步业务的典型 Vuex 流程是:

组件 dispatch action
        ↓
action 执行异步操作
        ↓
action commit mutation
        ↓
mutation 同步修改 state
        ↓
组件响应更新

Pinia 保留了 state、getters 和 actions,但取消了单独的 mutation 层:

组件调用 action
        ↓
action 同步或异步处理业务
        ↓
直接修改 state
        ↓
组件响应更新

新项目通常优先使用 Pinia,存量项目则经常遇到 Vuex。对于前端秋招,不仅要会说“Pinia 更简洁”,还要能清楚解释 Vuex 为什么通过 mutation 保证同步状态变更,以及两套方案的完整数据流。

无论使用哪一种状态管理库,都不要把所有数据全局化。局部状态留在组件中,真正需要跨组件、跨页面共享的业务状态再进入 store。


参考资料