一文带你了解前端全局状态管理

一文带你了解Vue全局状态管理

1. 背景

  1. 在前端开发中,状态管理始终是架构设计的核心议题之一。早期应用通常依赖全局变量与直接的 DOM 操作维护状态:在页面规模较小时尚可工作,但随着 SPA 普及与业务复杂度上升,状态逐渐分散在多个组件与服务调用链路中,导致"数据从哪里来、被谁改过、何时改的"难以追踪,跨组件共享与异步副作用也更难治理。为应对这些问题,业界逐步形成了以 单向数据流集中式/半集中式存储可追踪变更 为目标的状态管理方案,如 MobX、Redux、Vuex、Pinia、Zustand、Recoil 等。

2. Vuex

2.1 Vuex 主要是为了解决什么问题?

  1. 在 Vue 2 时代,当应用稍微变大时,会立刻遇到几个痛点:

    1. 跨组件共享状态极其麻烦

      1. user / cart / token 等状态在很多页面和组件里都要用。

      2. 对于相邻的父子组件、对于多层级嵌套的场景下,你要么在根组件维护,然后一层层 props​ 往下传,结构一变,整条传参链都要改;要么搞一个 event bus,到处 $emit​ / $on

    2. 状态修改没有规范和约束,调试困难

      1. 任意组件都可以随便改一个"全局对象"(比如 window.user​、this.$root.xxx​),出问题的时候,你根本不知道是哪个组件在什么时候把它改坏的

        1. window.user 这类全局对象 不在 Vue 响应式体系内,改了不一定触发视图更新
        2. this.$root.xxx 如果随处可写,就会出现"改坏了但不知道谁改的"
        3. Vue 2 还有一个额外坑:给响应式对象新增属性 需要 Vue.set / this.$set 才能被追踪
    3. 复杂业务的异步逻辑散落在各处

      1. 请求逻辑、缓存逻辑、权限逻辑都写在组件里;
      2. 一份业务规则要在多个组件拷贝/同步,难以复用。
  2. Vuex 的解决思路

    1. 统一搞一个 全局 store :所有全局状态放在 state对象 里。

      我们经常说要把数据交给Vuex进行管理,其实就是把数据交给Vuex里面的State对象进行保管

    2. 规定所有同步修改必须通过 mutation ;异步/业务逻辑通过 action

    3. 提供 getter 做派生状态(mapGetters)

      不要把所有需要用到的值都直接存进 state,而是只存"最小必要的原始状态",

      对于那些可以由原始状态计算出来的"衍生信息",统一通过 Vuex 的 getter 封装和计算。

    4. 配合 DevTools 做时间旅行调试(每次 mutation 都能记录和回放)。

2.2 Vuex基本概念

  1. 概念 :Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

  2. Vuex它的背后基本思想借鉴了Flux。Flux 是 Facebook 在构建大型 Web 应用程序时为了解决数据一致性 问题而设计出的一种架构,它是一种描述状态管理的设计模式。绝大多数前端领域的状态管理工具都遵循这种架构 ​(如Redux, MobX, Recoil, Zustand, otai, Vuex, Pinia)

    • Flux 架构主要有四个组成部分:

      • store:状态数据的存储管理中心,可以有多个,可以接受 action 做出响应。
      • view:视图,根据 store 中的数据渲染生成页面,与 store 之间存在发布订阅关系。
      • action:一种描述动作行为的数据对象,通常会包含动作类型 type 和需要传递的参数 payload 等属性。
      • dispatcher :调度器,接收 action 并分发至 store。
    • Flux 架构最核心的特点:单向数据流

  3. 单向数据流

    1. 状态,驱动应用的数据源;

    2. 视图 ,以声明方式将状态映射到视图;

    3. 操作 ,响应在视图 上的用户输入导致的状态变化

    4. 我对单向数据流的理解是应用里数据的"产生、更新、传播到界面"的路径只有一个固定方向 ,而不是在多个方向来回修改同一份状态。用链路来表示:View(用户交互) → Action(发起意图) → Mutation(落库修改) → State 更新 → View 重新渲染, 你可以把它理解成"数据的高速公路只有一条,别在路上逆行"

      整个数据流动关系为:

      1、view 视图中的交互行为会创建 action,交由 dispatcher 调度器。

      2、dispatcher 接收到 action 后会分发至对应的 store。

      3、store 接收到 action 后做出响应动作,并触发 change 事件,通知与其关联的 view 重新渲染内容。

  4. 原理图 ​

    1. Vuex在沿用 Flux 的思想的基础上针对 Vue 框架单独进行了一些设计上的优化,Vuex主要由四部分组成

      1. State:状态,Vuex 使用单一状态树------Vuex用一个对象就包含了全部的应用层级状态。至此它便作为一个"唯一数据源 "而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段

      2. Getter:有时候我们需要从 store 中的 state 中派生出一些状态,此时会使用到Getter。Getter等效于 Vue 组件中的计算属性。它会自动收集依赖,以实现计算属性的缓存。

      3. Mutations​:更改 Vuex​ 的 store​ 中的状态的唯一方法是提交 mutation​。作用是:修改、加工、维护数据。在 Vuex 中,mutation 都是同步事务

      4. Actions​:动作、行为。使用场景:涉及到调用异步 API分发多重 mutation

        1. Action 提交的是 mutation,而不是直接变更状态。

        2. Action 可以包含任意异步操作。

          js 复制代码
          actions: {
            checkout ({ commit, state }, products) {
              // 把当前购物车的物品备份起来
              const savedCartItems = [...state.cart.added]
              // 发出结账请求
              // 然后乐观地清空购物车
              commit(types.CHECKOUT_REQUEST)
              // 购物 API 接受一个成功回调和一个失败回调
              shop.buyProducts(
                products,
                // 成功操作
                () => commit(types.CHECKOUT_SUCCESS),
                // 失败操作
                () => commit(types.CHECKOUT_FAILURE, savedCartItems)
              )
            }
          }

      5. Module:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

        为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块------从上至下进行同样方式的分割

2.3 Vuex的使用

  1. 对于大型应用,Vuex​ 会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:

    js 复制代码
    ├── index.html
    ├── main.js
    ├── api
    │   └── ... # 抽取出API请求
    ├── components
    │   ├── App.vue
    │   └── ...
    └── store
        ├── index.js          # 我们组装模块并导出 store 的地方
        ├── actions.js        # 根级别的 action
        ├── mutations.js      # 根级别的 mutation
        └── modules
            ├── cart.js       # 购物车模块
            └── products.js   # 产品模块
  2. 基本使用

    1. store/index.js​:把各个模块、插件和全局配置组装成一个 store 实例并导出

      js 复制代码
      import Vue from 'vue'
      import Vuex from 'vuex'
      Vue.use(Vuex)
      
      const actions = {}
      const mutations = {}
      const state = {}
      
      export default new Vuex.Store({
      	actions,
      	mutations,
      	state
      })
    2. main.js​中创建vm时传入store配置项

      js 复制代码
      ......
      //引入store
      import store from './store'
      ......
      
      //创建vm
      new Vue({
      	el:'#app',
      	render: h => h(App),
      	store
      })
    3. store/index.js​:初始化数据、配置actions​、配置mutations

      js 复制代码
      import Vue from 'vue'
      import Vuex from 'vuex'
      Vue.use(Vuex)
      
      // 配置actions
      const actions = {
      	addWait(context,value){
      		setTimeout(()=>{
      			context.commit('add',value)
      		},500)
      	}
      }
      
      // 配置mutations
      const mutations = {
      	add(state,value){
      		state.sum += value
      	}
      }
      
      // 配置状态
      const state = {
      	sum:0
      }
      
      
      export default new Vuex.Store({
      	actions,
      	mutations,
      	state,
      })
    4. components/AppCount.vue

      • 通过 this.$store.dispatch 调用 action 中的方法
      • 通过 this.$store.commit 调用 mutations 中的方法
      • 通过 $store.state.sum 读取 state
      js 复制代码
      <template>
          <div>
              <h1>当前求和为:{{$store.state.sum}}</h1>
              <button @click="addNumber">+1</button>
              <button @click="addNumberWait">等1秒加1</button>
          </div>
      </template>
      
      <script>
          export default {
              name:'AppCount',
              data() {
                  return {
                  }
              },
              methods: {
                  addNumber(){
                      this.$store.commit('add',1)
                  },
                  addNumberWait(){
                      this.$store.dispatch('addWait',1)
                  }
              },
          }
      </script>
      
      <style lang="css">
          button{
              margin-right: 5px;
          }
      </style>>
  3. Getter:有时候我们需要从 store 中的 state 中派生出一些状态,此时会使用到Getter

    1. store/index.js

      js 复制代码
      import Vue from 'vue'
      import Vuex from 'vuex'
      Vue.use(Vuex)
      
      ...
      // 新增getter对象
      const getters = {
          bigSum(state){
              return state.sum * 10
          }
      }
      ...
      const state = {
      	sum:0
      }
      
      
      export default new Vuex.Store({
      ...
      	getters // 导出getter
      })
    2. components/AppCount.vue

      js 复制代码
      <template>
          <div>
              <h1>当前求和为:{{$store.state.sum}}</h1>
              <!-- 读取getter内容 -->
              <h1>当前大十倍:{{$store.getters.bigSum}}</h1>
              <button @click="addNumber">+1</button>
              <button @click="addNumberWait">等1秒加1</button>
          </div>
      </template>
      
      <script>
          export default {
              name:'AppCount',
              data() {
                  return {
                  }
              },
              methods: {
                  addNumber(){
                      this.$store.commit('add',1)
                  },
                  addNumberWait(){
                      this.$store.dispatch('addWait',1)
                  }
              },
          }
      </script>
      
      <style lang="css">
          button{
              margin-right: 5px;
          }
      </style>>
  4. 模块化

    1. Module:当项目变大,单一 store 会臃肿。Vuex 提供 modules 来拆分。

    2. 基本使用

      1. 代码目录

        js 复制代码
        .
        ├── App.vue
        ├── components
        │   └── AppCount.vue
        ├── main.js
        └── store
            ├── index.js
            └── modules
                ├── code.js
                └── count.js
      2. 在初始化实例的时候,Store​允许我们传入modules 字段

        js 复制代码
        export interface StoreOptions<S> {
          state?: S | (() => S);
          getters?: GetterTree<S, S>;
          actions?: ActionTree<S, S>;
          mutations?: MutationTree<S>;
          modules?: ModuleTree<S>;  // 传入modules ✅
          plugins?: Plugin<S>[];
          strict?: boolean;
          devtools?: boolean;
        }
      3. store/index.js:在modules,我们可能放多个对象

        js 复制代码
        import Vue from 'vue'
        import Vuex from 'vuex'
        Vue.use(Vuex)
        import countModules from './modules/count'
        import codeModules from './modules/code'
        export default new Vuex.Store({
        	modules: {  // 配置modules字段 ✅
        		countModules,
        		codeModules
        	}
        })
      4. store/modules/count.js

        js 复制代码
        export default {
          actions: {
            addWait(context, value) {
              setTimeout(() => {
                context.commit("add", value);
              }, 500);
            },
          },
        
          mutations: {
            add(state, value) {
              state.sum += value;
            },
          },
        
          getters: {
            bigSum(state) {
              return state.sum * 10;
            },
          },
        
          state: {
            sum: 0,
          },
        };
    3. 支持模块相互嵌套

      1. src/store/modules/code.js

        js 复制代码
        // hlg:6;
        import codexModules from './codex'
        export default {
          namespaced: true,
          modules: {
            codexModules, // ✅ 新增子模块
          },
          ...
        };
      2. 在可以直接通过store读到嵌套模块的数据

        js 复制代码
        // hlg:3; hlg:6;
        // 1. 在模板里直接读
        <h1>codexModules的代码行数为:{{ $store.state.codeModules.codexModules.codexNum }} </h1>
        // 2. 在 Composition API 里用 computed
        const store = useStore()
        const codexNum = computed(() => store.state.codeModules.codexModules.codexNum)
        // 其他方式同理
    4. 动态注册模块

      1. 可以使用 module.registerModule 动态注册模块。实现按需引入

      2. 代码实现

        1. src/store/index.js:你需要改到Vuex Store 的入口文件,并且对外暴露动态注册、动态注销的方法

          js 复制代码
          // hlg:5; hlg:10; hlg:19
          const store = createStore({
            modules: {
              // ✅ 静态注册你"始终需要"的模块
              countModules
            }
          })
          
          // ✅ 动态注册入口:可在 main.js 或某个页面/组件进入时调用
          export function registerCodeModules() {
            // Vuex4 支持 hasModule,用它避免重复注册
            if (!store.hasModule('codeModules')) {
              store.registerModule('codeModules', codeModules)
            }
          }
          
          
          // ✅ 动态注销该模块
          export function unregisterCodeModules() {
            if (store.hasModule('codeModules')) {
              store.unregisterModule('codeModules')
            }
          }
          
          // ✅ 默认导出 store 实例
          export default store
        2. 使用场景:

          1. 你可以在应用启动时就注册按需注册某个模块。 既然你选择"启动即加载",通常意味着这个模块会贯穿整个应用生命周期。因此一般来说不需要卸载

            js 复制代码
            // src/main.js
            import store, { registerCodeModules } from './store'
            // ✅ 在应用启动时动态注册
            registerCodeModules()
            const app = createApp(App)
            app.use(store)
            app.mount('#app')
          2. 当然,你也可以在进入组件时才动态注册

            1. 在路由守卫 beforeEnter 时动态注册

              js 复制代码
              import AppCount from '@/components/AppCount.vue'
              import { registerCodeModules } from '@/store'
              
              const routes = [
                {
                  path: '/count',
                  component: AppCount,
                  beforeEnter: () => {
                    registerCodeModules()
                    return true
                  },
                },
              ]
            2. 离开组件路由时beforeRouteLeave卸载

              js 复制代码
              import { unregisterCodeModules } from '@/store'
              
              export default {
                beforeRouteLeave(to, from, next) {
                  unregisterCodeModules()
                  next()
                },
              }
  5. 命名空间

    1. 命名空间:在 Vuex 中开启 namespaced: true​ 的作用是:把该模块的 state/getters/mutations/actions "隔离到自己的命名空间"里,从而避免与其他模块发生命名冲突

    2. 如果你不开启命名冲突,你根本不知道你调用的是啥。

      1. $store.getters.bigSum : 会执行模块注册顺序第一个实现getter方法的store里的getter返回的数据
      2. this.$store.commit('add',1)this.$store.dispatch('addWait',1) : 会按照模块注册顺序,执行顺序actionsmutations方法
    3. 开启命名空间之后,你就可以分模块去调用对应的的getters​、commit​、dispatch

      js 复制代码
      // hlg:6; hlg:23; hlg:26;
      <template>
          <div>
              <h1>当前求和为:{{$store.state.countModules.sum}}</h1>
              <!-- 读取getter内容 -->
              <h1>当前大十倍:{{$store.getters["countModules/bigSum"]}}</h1>
      
              <h1>当前代码行数为:{{$store.state.codeModules.codeNum}}</h1>
              <button @click="addNumber">+1</button>
              <button @click="addNumberWait">等1秒加1</button>
          </div>
      </template>
      
      <script>
          export default {
              name:'AppCount',
              data() {
                  return {
                  }
              },
              methods: {
                  addNumber(){
                      this.$store.commit('countModules/add',1)
                  },
                  addNumberWait(){
                      this.$store.dispatch('countModules/addWait',1)
                  }
              },
          }
      </script>
      
      <style lang="css">
          button{
              margin-right: 5px;
          }
      </style>>
  6. map方法

    1. 由于每次this.$store.xxx​非常麻烦,产生大量重复无用代码,因此Vuex提供了4个辅助函数 mapState​ / mapGetters​ / mapActions​ / mapMutations

    2. mapState​:把 store.state​ 映射成组件的 computed(响应式读取)。

    3. mapGetters​:把 store.getters​ 映射成组件的 computed

    4. mapActions​:把 store.dispatch()​ 映射成组件的 methods

    5. mapMutations​:把 store.commit()​ 映射成组件的 methods

    6. 代码示例

      js 复制代码
      // hlg:4; hlg:6; hlg:23; hlg:24; hlg:27; hlg:28; hlg:30; hlg:33;
      <template>
          <div>
              <h1>当前求和为:{{sum}}</h1>
              <!-- 读取getter内容 -->
              <h1>当前大十倍:{{bigSum}}</h1>
      
              <h1>当前代码行数为:{{$store.state.codeModules.codeNum}}</h1>
              <button @click="addNumber">+1</button>
              <button @click="addNumberWait">等1秒加1</button>
          </div>
      </template>
      
      <script>
          import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
          export default {
              name:'AppCount',
              data() {
                  return {
                  }
              },
              computed: {
                  ...mapState('countModules', ['sum']),
                  ...mapGetters('countModules', ['bigSum']),
              },
              methods: {
                  ...mapMutations('countModules', ['add']),
                  ...mapActions('countModules', ['addWait']),
                  addNumber(){
                      this.add(1);
                  },
                  addNumberWait(){
                      this.addWait(1)
                  }
              },
          }
      </script>
      
      <style lang="css">
          button{
              margin-right: 5px;
          }
      </style>>
    7. 因为这些 mapXxx​ 辅助函数是为 Options API 设计的,内部依赖组件实例的 this.$store​;而在Vue3 Composition API setup()​ 里没有 this​。一次Vue3 Composition API 不能使用mapXxx这些辅助函数

  7. Vue3 Composition API​ 中使用store

    1. 在Vue2中,安装的一般是Vuex 3.xxx的版本。Vuex 3.xxx的版本主要是用于 Vue 2 生态

    2. 而如果项目是Vue3,需要安装Vuex 4,Vuex 4重点支持 Vue 3,并尽量保持与 Vuex 3 相同 API 以便复用代码

    3. 基本使用

      1. main.js​:通过app.use(store)​ 把 store​ 这个插件挂到当前应用实例 app

        js 复制代码
        import { createApp } from 'vue'
        import App from './App.vue'
        import store from './store'
        
        const app = createApp(App)
        app.use(store)
        app.mount('#app')
      2. store里的所有代码不需要改动

      3. src/components/AppCount.vue​:Vuex 4 引入了一个新的 API 用于在组合式 API 中与 store 进行交互。可以在组件的 setup 钩子函数中使用 useStore 组合式函数来检索 store。通过const store = useStore()获取store实例

        js 复制代码
        // hlg:21; hlg:24; hlg:27; hlg:30; hlg:8;
        <template>
          <div>
            <h1>当前求和为:{{ sum }}</h1>
            <!-- 读取getter内容 -->
            <h1>当前大十倍:{{ bigSum }}</h1>
        
            <h1>当前代码行数为:{{ $store.state.codeModules.codeNum }}</h1>
            <button @click="addNumber">+1</button>
            <button @click="addNumberWait">等1秒加1</button>
          </div>
        </template>
        
        <script setup>
        import { computed } from 'vue'
        import { useStore } from 'vuex'
        
        const store = useStore()
        
        // 等价于 Vue2 的:...mapState('countModules', ['sum'])
        const sum = computed(() => store.state.countModules.sum)
        
        // 等价于 Vue2 的:...mapGetters('countModules', ['bigSum'])
        const bigSum = computed(() => store.getters['countModules/bigSum'])
        
        // 等价于 Vue2 的:...mapMutations('countModules', ['add'])
        const add = (value) => store.commit('countModules/add', value)
        
        // 等价于 Vue2 的:...mapActions('countModules', ['addWait'])
        const addWait = (value) => store.dispatch('countModules/addWait', value)
        
        function addNumber() {
          add(1)
        }
        
        function addNumberWait() {
          addWait(1)
        }
        </script>
        
        <style lang="css">
        button {
          margin-right: 5px;
        }
        </style>
    4. 注意:直接在JS代码里执行const sum1 = store.state.countModules.sum;​ 或者解构const { sum } = store.state.countModules​ 获取到的是数值基本类型(getter​也是同理),如果需要将获取到的值直接绑定到模板是不具备响应式的 ,如果你解出来的数据需要响应式可以直接通过toRefs​先将store​的数据变成响应式(或者使用toRef、computed、watch)

      js 复制代码
      // 直接解构,如果直接赋值是不具备相应是的
      const sum1 = store.state.countModules.sum;
      const { sum } = store.state.countModules
      // 方法1
      const { sum } = toRefs(store.state.countModules);
      // 方法2
      const sum = toRef(store.state.countModules, 'sum')
      // 方法3:computed
      ...略
      // 方法4:watch
      ...略

3. Pinia

3.1 背景

Vue 的官方状态管理库已经改为 Pinia。Pinia 的 API 与 Vuex 5(在 Vuex 5 RFC 中描述)几乎完全相同,甚至有所增强。你可以把 Pinia 简单理解为"改了名字的 Vuex 5"。Pinia 也同样支持 Vue 2.x。

  1. 从官方的话可以知道Pinia 吸收了 Vuex 5 RFC 的设计方向,并最终成为 Vue 官方推荐的状态管理方案。 目前Pinia GitHub仓库已经纳入vuejs组织下
  2. 官方文档:pinia.vuejs.org/
  3. 为什么取名 Pinia? Pinia是西班牙语中Piña(菠萝)
  4. Pinia是什么? Pinia The intuitive store for Vue.js
  5. Pinia设计理念 : Type Safe, Extensible, and Modular by design. Forget you are even using a store.

3.2 Pinia组成

  1. Pina主要有三个组成部分:

    • state:store 的核心,与 Vue 中的 data 一致,可以直接对其中的数据进行读写。
    • getters:与 Vue 中的计算属性相同,支持缓存。
    • actions:操作不受限制,可以创建异步任务,可以直接被调用,不再需要 commit、dispatch 等方法。可在action中直接改 state

下面我们来看看Pinia主要解决了什么问题?

3.3 Pinia 主要是为了解决什么问题?

  1. Pinia 主要解决的问题如下:

    1. 去掉 mutations 的外部接口,减少冗余分层
    2. 更贴合 Composition APIuseXxxStore() + storeToRefs(),语法更简洁
    3. 减少"魔法字符串路径",提升重构可靠性
    4. 扁平化多 store 结构,降低模块嵌套与动态注册心智负担
    5. 降低 TypeScript 使用成本,更多依赖类型推导
  2. 改进1: 去掉 mutations​,逻辑更聚合,mutations​被认为非常冗长,因此在Pinia​中不再存在mutations

    Pinia 并非完全抛弃了 mutation​,而是将对 state​ 中单个数据进行修改的操作封装为一个 mutation​,但不对外开放接口。可以在 devtools​ 中观察到 mutation

    1. Vuex 4:必须拆 mutation + action:

      js 复制代码
      mutations: {
        add(state, value) { state.sum += value }
      },
      actions: {
        addWait({ commit }, value) { setTimeout(() => commit('add', value), 500) }
      }
    2. Pinia:直接在 action 里改 state:

      js 复制代码
      actions: {
        add(value) { this.sum += value },
        addWait(value) { setTimeout(() => this.add(value), 500) }
      }
  3. 改进2: 语法更简洁,提供 Composition-API 风格 API

    1. Vuex 4

      1. 需要写大量重复代码store.state.xxx​、store.getters.xxx​、store.commit('xxx', value)​、store.dispatch('xxx', xxx)

      2. Composition API:useStore + computed + commit/dispatch

      3. Composition API:useStore + ToRefs

        js 复制代码
        <script setup>
        import { computed } from 'vue'
        import { useStore } from 'vuex'
        const store = useStore()
        const sum1 = computed(() => store.state.countModules.sum)
        const { sum } = store.state.countModules
        const bigSum = computed(() => store.getters['countModules/bigSum'])
        const add = (value) => store.commit('countModules/add', value)
    2. Pinia

      1. useCounterStore + storeToRefs + 直接调用 action

        js 复制代码
        <script setup>
        import { computed } from 'vue'
        import { storeToRefs } from 'pinia'
        import { useCountModulesStore } from '@/stores/countModules'
        
        const store = useCountModulesStore()
        
        // ✅ 等价:computed(() => store.state.countModules.sum)
        const sum1 = computed(() => store.sum)
        
        // ✅ 等价:const { sum } = store.state.countModules 但保持响应式
        const { sum, bigSum } = storeToRefs(store)
        
        // ✅ 等价:commit('countModules/add', value)
        const add = (value) => store.add(value)
        </script>
  4. 改进3: 解决"magic strings(字符串路径)多、重构脆弱":直接 import store + 自动补全

    1. Pinia 官方明确写了:No more magic strings (不再靠字符串注入/路径),改为"导入函数、调用、享受自动补全"。

    2. 而 Vuex 4.xx 的模块/命名空间场景下,经常需要 'a/b/c/someGetter' 这种路径字符串,官方模块文档也提到会变得冗长。

    3. Vue4

      js 复制代码
      const bigSum = computed(() => store.getters['countModules/bigSum'])
      store.dispatch('countModules/addWait', 1)
      store.commit('countModules/add', 1)
    4. Pinia

      js 复制代码
      const { sum, bigSum } = storeToRefs(countStore)
      countStore.addWait(1)
      countStore.add(1)
  5. 改进4: 解决"嵌套 modules + namespaced 心智负担":扁平多 store 结构,天然隔离。在Pinia中不需要动态添加 Store,它们默认是动态的。

    1. 不再需要嵌套模块结构 ,默认扁平;不再需要 namespaced modules,因为 store 的命名空间天然由 store id 决定。

    2. Vuex 的模块体系支持嵌套与命名空间,并提供 rootState/rootGetters 等机制。

    3. Vuex4必须集中在一个 createStore 里注册 modules,或者自己写动态注册的逻辑

      js 复制代码
      // 保留注册方法
      const store = createStore({
        modules: {
          countModules
        }
      })
      export function registerCodeModules() {
        // Vuex4 支持 hasModule,用它避免重复注册
        if (!store.hasModule('codeModules')) {
          store.registerModule('codeModules', codeModules)
        }
      }
      
      // 业务使用时注册Modules
      import store from '@/store'
      import codeModules from '@/store/modules/code'
      
      store.registerCodeModules('codeModules', codeModules)
    4. Pinia不需要集中注册:

      1. stores/countModules.js 就是一个 store

      2. stores/codeModules.js 就是一个 store

      3. 用到哪里就 useXXXStore(),天然按需引入

        js 复制代码
        import { useCodeModulesStore } from '@/stores/codeModules'
        const codeStore = useCodeModulesStore() // 用到即生效
  6. 改进5: 解决"TypeScript 支持成本高":尽量靠类型推导而不是自定义复杂封装,不需要为 TS 创建复杂封装;

3.4 Pinia的使用

  1. src/main.js: 创建一个 pinia 实例,把 pinia 注入到整个应用

    • createPinia()
      • Pinia API:创建一个 pinia 实例(全局 store 容器)。
      • 这个实例会被 app.use(pinia) 安装到 Vue 应用中。
    js 复制代码
    import { createApp } from 'vue'
    import { createPinia } from 'pinia'
    import App from './App.vue'
    const app = createApp(App)
    // 创建一个 pinia 实例
    const pinia = createPinia()
    app.use(pinia)
    app.mount('#app')
  2. src/stores/counter.js​:通过defineStore 定义Store,传入id作为store 的唯一标识。在组件中调用 useXXXStore()​ 获取 store 实例(响应式)。

    1. Composition API 风格

      可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。

      js 复制代码
      import { defineStore } from 'pinia'
      import { ref, computed } from 'vue'
      
      export const useCounterStore = defineStore('counter', () => {
        // state
        const sum = ref(0)
      
        // getters
        const bigSum = computed(() => sum.value * 10)
      
        // actions
        function add(value) {
          sum.value += value
        }
      
        function addWait(value) {
          setTimeout(() => {
            add(value)
          }, 500)
        }
      
        return { sum, bigSum, add, addWait }
      })
    2. Options API 风格

      也可以传入一个带有 state、actions 与 getters 属性的 Option 对象

      js 复制代码
      import { defineStore } from 'pinia'
      
      // 定义Store
      export const useCounterStore = defineStore('counter', {
      // state
        state: () => ({
          sum: 0
        }),
      // getters
        getters: {
          bigSum: (state) => state.sum * 10
        },
      
      // actions
        actions: {
          add(value) {
            this.sum += value
          },
      
          addWait(value) {
            // 保持你原项目的行为:延迟 500ms 再 +value
            setTimeout(() => {
              this.add(value)
            }, 500)
          }
        }
      })


    3. 在 Pinia 的 setup store(Composition API 风格)内部,直接使用 Vue 的 watch / watchEffect 去监听 store 的状态(ref/computed),一旦状态变化就自动执行副作用逻辑 (例如:持久化、请求接口、同步路由、打点、节流防抖等)。这类 watcher 不需要写在组件里 ,因此只要 store 被创建一次,监听逻辑就会持续生效(对所有使用该 store 的组件都统一生效)。

    js 复制代码
    // stores/counter.js
    import { defineStore } from 'pinia'
    import { ref, computed, watch } from 'vue'
    
    export const useCounterStore = defineStore('counter', () => {
      // state
      const sum = ref(0)
    
      // 初始化:从 localStorage 读回(注意 SSR 需要做 window 判断)
      if (typeof window !== 'undefined') {
        const cached = window.localStorage.getItem('sum')
        if (cached != null) sum.value = Number(cached) || 0
      }
    
      // getter
      const bigSum = computed(() => sum.value * 10)
    
      // actions
      function add(value) {
        sum.value += value
      }
    
      function addWait(value) {
        setTimeout(() => add(value), 500)
      }
    
      // watcher:监听 sum 变化,自动持久化
      watch(
        sum,
        (newVal, oldVal) => {
          // 这里就是"store 内创建侦听器"的含义:状态变了,副作用自动执行
          if (typeof window !== 'undefined') {
            window.localStorage.setItem('sum', String(newVal))
          }
          // 你也可以在这里做打点、日志、联动请求等
          // console.log(`[counter] sum: ${oldVal} -> ${newVal}`)
        },
        { flush: 'post' } // 可选:让回调在组件更新后触发(非必须)
      )
    
      return { sum, bigSum, add, addWait }
    })
    
    // App.vue
    <script setup>
    import { useCounterStore } from './stores/counter'
    
    const counter = useCounterStore()
    </script>
    
    <template>
      <div>
        <p>sum: {{ counter.sum }}</p>
        <p>bigSum: {{ counter.bigSum }}</p>
        <button @click="counter.add(1)">+1</button>
        <button @click="counter.addWait(5)">500ms +5</button>
      </div>
    </template>
  3. src/components/AppCount.vue​:使用useXxxStore()

    js 复制代码
    <template>
      <div>
        <h1>当前求和为:{{ sum1 }}</h1>
        <!-- 读取 getter 内容(Pinia getter 可直接当作响应式值使用) -->
        <h1>当前大十倍:{{ bigSum }}</h1>
    
        <h1>codeModules的代码行数为:{{ codeNum }}</h1>
        <h1>codexModules的代码行数为:{{ codexNum }}</h1>
    
        <button @click="addNumber">+1</button>
        <button @click="addNumberWait">等1秒加1</button>
      </div>
    </template>
    
    <script setup>
    import { storeToRefs } from 'pinia'
    import { useCounterStore } from '../stores/counter'
    import { useCodeStore } from '../stores/code'
    import { useCodexStore } from '../stores/codex'
    
    /**
     * useXxxStore()
     * - Pinia API:获取/创建 store 实例(按需创建)。
     * - store 实例本身是响应式对象。
     */
    const counterStore = useCounterStore()
    const codeStore = useCodeStore()
    const codexStore = useCodexStore()
    
    /**
     * storeToRefs(store)
     * - Pinia API:把 store 上的 state/getters 转为 refs,便于解构且不丢响应式。
     * - 推荐用法:const { a, b } = storeToRefs(store)
     */
    const { sum, bigSum } = storeToRefs(counterStore)
    const { codeNum } = storeToRefs(codeStore)
    const { codexNum } = storeToRefs(codexStore)
    
    // 保持你原模板变量名 sum1(原来是 computed),这里直接复用 sum 这个 ref 即可
    const sum1 = sum
    
    function addNumber() {
      // Pinia:直接调用 action(不再 commit)
      counterStore.add(1)
    }
    
    function addNumberWait() {
      // Pinia:直接调用 action(不再 dispatch)
      counterStore.addWait(1)
    }
    </script>
    
    <style lang="css">
    button {
      margin-right: 5px;
    }
    </style>

4. 参考文档

facebookarchive.github.io/flux/?utm_s...

vuex.vuejs.org/

pinia.vuejs.org/

blog.isquaredsoftware.com/presentatio...

jelly.jd.com/article/634...

相关推荐
柳安2 小时前
对keep-alive的理解,它是如何实现的,具体缓存的是什么?
前端
keyV2 小时前
告别满屏 v-if:用一个自定义指令搞定 Vue 前端权限控制
前端·vue.js·promise
X_Eartha_8152 小时前
前端学习—HTML基础语法(1)
前端·学习·html
如果你好2 小时前
一文搞懂事件冒泡与阻止方法:event.stopPropagation() 实战指南
前端·javascript
用户8168694747252 小时前
深入 useMemo 与 useCallback 的底层实现
前端·react.js
AAA简单玩转程序设计2 小时前
救命!Java 进阶居然还在考这些“小儿科”?
java·前端
MediaTea2 小时前
思考与练习(第十章 文件与数据格式化)
java·linux·服务器·前端·javascript
JarvanMo2 小时前
别用英语和你的大语言模型说话
前端
江公望2 小时前
Vue3的 nextTick API 5分钟讲清楚
前端·javascript·vue.js