Vue 和 React中的状态管理机制

Vuex、Pinia、Context、Redux

最近自学了React框架的基本知识,学完Redux后对这四种状态管理机制都有了基本了解,为了更好地理解这四种机制的差别,对笔记进行了整理和归纳,方便纵向和横向对比。

VUEX

Vuex 是 Vue.js 的官方状态管理库,专为复杂的中大型单页应用(SPA)设计。实际开发中是多个组件共享一个状态,可以使用vuex进行状态管理,负责组件中的通信,方便维护代码。

需要vuex的场景:

  • 多个组件依赖同一状态(如用户登录信息)
  • 不同组件需要修改同一份数据(如购物车商品)
  • 需要记录状态变化历史(如时间旅行调试)
  • 跨层级组件通信困难(避免深层次的 props/$emit

1.基础使用

  • 在vue中添加vuex插件。可以通过vue-cil添加vuex,也可以通过npm安装vuex。
  • 在开发环境开启严格模式,修改数据时就必须通过mutation来处理。
scss 复制代码
strict:products.env.NODE_ENV !== 'production'

在项目的 src 目录下会多出一个 store 目录,目录下会有个 index.js

javascript 复制代码
/* src/store/index.js */
​
// 导入 Vue
import Vue from 'vue'
// 导入 Vuex 插件
import Vuex from 'vuex'
​
// 把 Vuex 注册到Vue 上
Vue.use(Vuex)
​
export default new Vuex.Store({
  // 在开发环境开启严格模式 这样修改数据 就必须通过 mutation 来处理
  strict:products.env.NODE_ENV !== 'production',
  // 状态
  state: {
  },
  // 用来处理状态
  mutations: {
  },
  // 用于异步处理
  actions: {
  },
  // 用来挂载模块
  modules: {
  }
})
​

使用的时候就把store挂载到vue中,所有组件就可以从store中获取数据。

2.核心概念

state

  • vuex中的公共状态,所有共享数据存储于此。
  • 特点:响应式,类似组件的data
php 复制代码
import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex'
​
Vue.use( Vuex )
​
const store = new Vuex.Store({
state:{ 
  products: [
    {name: '鼠标', price: 20},
    {name: '键盘', price: 40},
    {name: '耳机', price: 60},
    {name: '显示屏', price: 80}
  ]
}
})
​
new Vue({
el: '#app',
store,
render: h => h(App)
})
​

Getters

是vuex的计算属性,类似于computed。用于封装复杂逻辑或过滤数据(派生状态)。

getters的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

使用方法:

javascript 复制代码
// 先写入getters
Vue.use(Vuex)
​
export default new Vuex.Store({
  state: {
    name: '张三',
    age: 21,
  },
  getters: {
    // 在这里对state进行包装
    //state 状态 如果要使用 state 里面的数据,第一个参数默认就是 state ,名字随便取
    decorationName(state) {
      return `大家好我的名字叫${state.name}今年${state.age}岁`
    },
  },
})
​
//使用:
//1.导入:this.$store.getters[名称]
<template>
  <div id="app">
    {{ this.$store.getters.decorationName }}
  </div>
</template>
​
​
//2.使用 mapGetters
<template>
  <div id="app">
    {{ decorationName }}
  </div>
</template>
​
<script>
​
// 从 Vuex 中导入 mapGetters
import { mapGetters } from 'vuex'
export default {
  name: 'App',
  computed: {
    // 将 getter 映射到当前组件的计算属性
    ...mapGetters(['decorationName'])
  },
}
</script>
​

mutation

store中的状态不能直接对其进行操作,需要使用mutation来对store中的状态进行修改,方便集中监控数据的变化。

state的更新必须是 mutation来处理。mutations对象中保存着更改数据的回调函数,该函数名官方规定叫type, 第一个参数是state, 第二参数是payload, 也就是自定义的参数.

使用方法:

javascript 复制代码
export default new Vuex.Store({
  state: {
    name: '张三',
    age: 21,
  },
  mutations: {
    // 在这里定义 方法
    /**
     * @param {*} state 第一个参数是 Store 中的状态(必须传递)
     * @param {*} newName 传入的参数 后面是多个
     */
    changeName(state, newName) {
      // 这里简单举个例子 修改个名字
      state.name = newName
    },
  },
  actions: {},
  modules: {},
})
​
​
//在组件中使用
<template>
  <div id="app">
    <button @click="handleClick">方式1 按钮使用 mutation 中方法</button>
    {{ name }}
  </div>
</template>
​
<script>
      
//1.this.$store.commit() 触发
​
// 从 Vuex 中导入 mapState
import { mapState } from 'vuex'
export default {
  name: 'App',
  computed: {
    // 将 store 映射到当前组件的计算属性
    ...mapState(['name', 'age'])
  },
  methods: {
      //用额外的方法使用vuex中的方法
    handleClick() {
      // 触发 mutations 中的 changeName
      this.$store.commit('changeName', '小浪')
    }
  },
}
</script>
​
​
//2.使用 mapMutations将mutation的方法直接映射到methods中
​
<script>
// 从 Vuex 中导入 mapState
import { mapState, mapMutations } from 'vuex'
export default {
  name: 'App',
  computed: {
    // 将 store 映射到当前组件的计算属性
    ...mapState(['name', 'age'])
  },
  methods: {
    // 将 mutations 中的 changeName 方法映射到 methods 中,就能直接使用了 changeName 了
    ...mapMutations(['changeName'])
  },
}
</script>
​

action

action用于处理异步任务,通过action触发mutation间接改变状态,不能直接使用mutation直接对异步任务进行修改

actions中的回调函数的第一个参数是context,是一个与store实例具有相同属性和方法的对象

定义actions方法:

javascript 复制代码
export default new Vuex.Store({
  state: {
    name: '张三',
    age: 21,
  },
  mutations: {
    // 在这里定义方法
    // state 第一个参数是 Store 中的状态(必须传递)
    // newName 传入的参数
    changeName(state, newName) {
      state.name = newName
    },
  },
  actions: {
    //context 上下文默认传递的参数
    //newName 自己传递的参数
​
    // 定义一个异步的方法 context是 store
    changeNameAsync(context, newName) {
      // 用 setTimeout 模拟异步
      setTimeout(() => {
        // 在这里调用 mutations 中的处理方法
        context.commit('changeName', newName)
      }, 2000)
    },
  },
  modules: {},
})
​

使用:

方法1:this.$store.dispatch()

xml 复制代码
//1.this.$store.dispatch()
​
<script>
​
// 从 Vuex 中导入 mapState mapMutations
import { mapState, mapMutations } from 'vuex'
export default {
  name: 'App',
  computed: {
    // 将 store 映射到当前组件的计算属性
    ...mapState(['name', 'age'])
  },
  methods: {
      //用一个新的方法间接调用
    changeName2(newName) {
      // 使用 dispatch 来调用 actions 中的方法
      this.$store.dispatch('changeNameAsync', newName)
    }
  },
}
</script>
​

方法2:mapAction

xml 复制代码
<script>
// 从 Vuex 中导入 mapState mapMutations mapActions
import { mapState, mapMutations, mapActions } from 'vuex'
export default {
  name: 'App',
  computed: {
    // 将 store 映射到当前组件的计算属性
    ...mapState(['name', 'age'])
  },
  methods: {
    // 直接映射 actions 中的指定方法 到 methods中,就可以在该组件直接使用
    ...mapActions(['changeNameAsync'])
  },
}
</script>

module

为了避免在一个复杂的项目state中的数据变得臃肿,vuex允许将store分成不同的模块,每个模块都有属于自己的 stategetteractionmutation

javascript 复制代码
//在index.js中挂载其他模块
​
//创建模块animal
/* animal.js */
​
const state = {
  animalName: '狮子',
}
const mutations = {
  setName(state, newName) {
    state.animalName = newName
  },
}
​
//导出
export default {
  state,
  mutations,
}
​

在index.js中引入:

javascript 复制代码
​
/* src/store/index.js */
​
// 引入模块
import animal from './animal'
​
// 把 Vuex 注册到Vue 上
Vue.use(Vuex)
​
export default new Vuex.Store({
  modules: {
    animal,
  },
})
​

然后就可以在组件中使用

方法1 :$store.state在module中挂载的模块名

javascript 复制代码
//1.$store.state[在module中挂载的模块名][挂载的模块里的属性]
<template>
  <div id="app">
    {{ this.$store.state.animal.animalName }}
    <button @click="$store.commit('setName', '老虎')">改名</button>
  </div>
</template>
​
//也可以使用mapXXX进行映射
//需要在导出时添加命名空间namespaced: true
​
/* animal.js */
const state = {
  animalName: '狮子',
}
const mutations = {
  setName(state, newName) {
    state.animalName = newName
  },
}
​
export default {
  // 开启命名空间 方便之后使用 mapXXX
  namespaced: true,
  state,
  mutations,
}
​
​

方法2 :使用mapState把属性导入到computed中,使用mapMutations把mutation中的方法导入到methods

xml 复制代码
//2.
<template>
  <div id="app">
    {{ animalName }}
    <button @click="setName('老鹰')">改名</button>
  </div>
</template>
​
<script>
​
// 从 Vuex 中导入 mapState mapMutations
import { mapState, mapMutations } from 'vuex'
export default {
  name: 'App',
  computed: {
    // mapState 使用方式和之前有些许不同,第一个是module挂载的模块名
    // 第二个参数是 animal 模块中的 state 属性
    ...mapState('animal', ['animalName'])
  },
  methods: {
    // mapMutations 使用方式也和之前有些许不同,第一个是module挂载的模块名
    // 第二个参数是 animal 模块中的 mutation 方法
    ...mapMutations('animal', ['setName'])
  },
}
</script>
​
​

3.工作流程

1.组件通过dispatch触发action

2.action执行异步操作

3.action通过commit提交mutation

4.mutation同步修改state

5.state变化触发所有依赖它的组件更新

Pinia

概念:pinia是vue的存储库,允许跨组件/页面共享,保留了 Vuex 的核心功能,将mutations与actions结合,提供了更简洁的 API 和更好的TypeScript 支持。

核心优势:

  • API简洁性,去掉了mutations
  • typescript支持
  • 模块化,自动按照文件分割模块
  • composition api:原生支持,与setup()完美结合
  • 体积更轻量
  • devtools支持

1.核心概念

store(存储)

核心单元,每个store是一个独立的状态容器

javascript 复制代码
// stores/counter.js
import { defineStore } from 'pinia'
​
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++ // 直接修改状态,无需 mutations
    }
  }
})

使用store

xml 复制代码
<script setup>
// 引入定义
import useFormInfoStore from '@/store/formInfo';
// 调用方法,返回store实例
const formInfoStore = useFormInfoStore();
</script>

解构store

xml 复制代码
<script setup>
import { storeToRefs } from 'pinia';
// 引入定义
import useFormInfoStore from '@/store/formInfo';
// 调用方法,返回store实例
const formInfoStore = useFormInfoStore();
​
const { name, age } = formInfoStore; // 此时解构出来的name和age不具有响应式
​
const { name, age } = storeToRefs(formInfoStore); //此时解构出来的name和age是响应式引用
</script>
state(状态)

使用函数返回初始状态(类似 Vue 的 data()),直接通过 store 实例访问和修改。

scss 复制代码
const store = useCounterStore()
console.log(store.count) // 访问
store.count++           // 直接修改(仍可追踪)

使用:

一般可以通过store实例来直接读取和写入状态:

xml 复制代码
<script setup>
import useFormInfoStore from '@/store/formInfo';
const formInfoStore = useFormInfoStore();
​
console.log(formInfoStore.name); // 'Hello World'
formInfoStore.age++;  // 19
</script>

还提供了一些常见场景的方法来操作:

  • $reset()重置状态变为初始值
  • $patch({param1,param2})对状态批量修改
  • $state(newState)通过将$state 属性设置为新对象来替换 Store 的整个状态
  • $subscribe()可以订阅store中的状态变化
xml 复制代码
<script setup>
import useFormInfoStore from '@/store/formInfo';
const formInfoStore = useFormInfoStore();
​
console.log(formInfoStore.name); // 'Hello World'
// 直接修改state中的属性
formInfoStore.age++;  // 19
​
// 1.$reset 重置状态,将状态重置成为初始值
formInfoStore.$reset();
console.log(formInfoStore.age); // 18
  
// 2.$patch 支持对state对象的部分批量修改
formInfoStore.$patch({
    name: 'hello Vue',
    age: 198
});
  
// 3.$state 通过将其 $state 属性设置为新对象来替换 Store 的整个状态
formInfoStore.$state = {
  name: 'hello Vue3',
  age: 100,
  gender: '男'
}
​
// 4.$subscribe 订阅store中的状态变化
formInfoStore.$subscribe((mutation, state) => {
  // 监听回调处理
}, {
  detached: true  // 💡如果在组件的setup中进行订阅,当组件被卸载时,订阅会被删除,通过detached:true可以让订阅保留
})
</script>
Getters(计算属性)

类似 Vue 的 computed,自动缓存结果

javascript 复制代码
getters: {
  doublePlusOne() {
    return this.double + 1 // 可调用其他 getter
  }
}

通常getter是不支持额外传参的,但是我们可以从getter返回一个函数的方式来接收参数:

javascript 复制代码
import { defineStore } from 'pinia';
​
const useFormInfoStore = defineStore('formInfo', {
    state: () => {
        return {
            name: 'Hello World',
            age: 18,
            isStudent: false,
            gender: '男'
        };
    },
    getters: {
        isLargeBySpecialAge: (state) => {
          return (age) => {
             return state.age > age
          }
        }
    }
});
​
export default useFormInfoStore;
actions(操作)

包含同步异步操作,可以直接修改state

csharp 复制代码
actions: {
  async fetchData() {
    const res = await api.getData()
    this.data = res // 直接赋值
  }
}

$onAction()

可以使用 store.$onAction() 订阅 action 及其结果。传递给它的回调在 action 之前执行。 after处理 Promise 并允许您在 action 完成后执行函数,onError允许您在处理中抛出错误。

javascript 复制代码
const unsubscribe = formInfoStore.$onAction(
  ({
    name, // action 的名字
    store, // store 实例
    args, // 调用这个 action 的参数
    after, // 在这个 action 执行完毕之后,执行这个函数
    onError, // 在这个 action 抛出异常的时候,执行这个函数
  }) => {
    // 记录开始的时间变量
    const startTime = Date.now()
    // 这将在 `store` 上的操作执行之前触发
    console.log(`Start "${name}" with params [${args.join(', ')}].`)
​
    // 如果 action 成功并且完全运行后,after 将触发。
    // 它将等待任何返回的 promise
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      )
    })
​
    // 如果 action 抛出或返回 Promise.reject ,onError 将触发
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      )
    })
  }
)
​
// 手动移除订阅
unsubscribe()

2.对比

  • Vue2和Vue3都支持,这让我们同时使用Vue2和Vue3的小伙伴都能很快上手。
  • Vuex中需要在actions中调用mutation修改state,但Pinia中可以直接修改state,实现同步异步操作,省略了mutation
  • Pinia中只有state、getter、action ,抛弃了Vuex中的Mutation,减少了工作量,更轻量
  • Pinia中action支持同步和异步,Vuex不支持
  • 良好的Typescript支持,Vue3都推荐使用TS来编写,这个时候使用Pinia就非常合适了
  • 无需再创建各个模块嵌套了,Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia中每个store都是独立的,互相不影响。
  • 体积非常小,只有1KB左右。
  • pinia支持插件来扩展自身功能。
  • 支持服务端渲染。

Context

Context是React提供的全局数据共享机制,用于在组件树中跨层级传递数据,不必通过 props一层层传递。

适用于存储:

  • 全局配置:主题、语言、用户认证状态
  • 共享服务:API客户端、日志服务
  • 复杂状态:购物车、用户偏好设置

创建Context

javascript 复制代码
import React from 'react'
​
// 创建一个context对象,参数是默认值,没有匹配到Provider的时候会提供这个值
const ThemeContext = React.createContext('light')
  • 创建后得到 { Provider, Consumer } 对象

Provider

在顶层提供数据

xml 复制代码
<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>

Consumer

可以在任意组件中使用,有两种使用方式

方式1useContext(推荐)

javascript 复制代码
import { useContext } from 'react'
​
function ChildComponent() {
  const theme = useContext(ThemeContext)
  return <div>当前主题是 {theme}</div>
}

方式2Context.consumer

xml 复制代码
<ThemeContext.Consumer>
  {theme => <div>当前主题是 {theme}</div>}
</ThemeContext.Consumer>

缺点

  • Provider的value发生变化时,所有使用了这个context的组件都会重新渲染,尽管有的组件没有使用到变化的属性。
javascript 复制代码
<ThemeContext.Provider value={{ theme: 'dark', toggle: fn }}>
  <CompA />  // 只使用了theme
  <CompB />  // 只使用了toggle
</ThemeContext.Provider>

解决方案:使用useMemo包裹value,减少不必要的更新

scss 复制代码
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme])
  • 难以做到模块化、精细化更新:

    context是一个全局单元,当管理的数据过多时,很难拆分、复用其中的数据,而Redux和Pinia可以做到模块化管理。

Redux

概念

redux是JavaScript应用的可预测状态容器,专为管理复杂应用状态而设计。

  • 单一数据源:它会将整个应用状态(数据)存储到一个地方,称为store。store中保存了一颗状态树(state tree)。
  • 状态只读 :组件改变state的唯一方法是通过调用store的dispatch方法,触发一个action,这个action被对应的reducer处理,state才完成更新
  • 使用纯函数执行修改reducer是纯函数,接收旧的state和action,返回新的state
  • 组件可以派发(dispatch)行为(action)给store,而不是直接通知其他组件
  • 其他组件则通过订阅store中的状态来刷新自己的视图

工作流程

  • view层触发dispatch(action)

    • action包含type和data
  • store调用reducer函数,传入当前的state和action

    • reducer用于出书化妆台、加工状态
  • reducer返回新的state

  • store保存新的state并通知所有订阅的组件

  • 更新视图:组件根据新state重新渲染

基础实现

  • 创建store:使用createStore函数创建一个store,传入一个reducer函数

    ini 复制代码
    const store = createStore(todosReducer);
    ​
  • 定义reducer:reducer是一个纯函数,根据action的类型更新状态。

    go 复制代码
    const reducer = (state=10,action) => {
        switch(action.type) {
            case 'INCREMENT': return state+1;
            case 'DECREMENT': return state-1;
            default:return state;
        }
    };
    ​
    <button onClick={()=>{
    store.dispatch({type:"INCREMENT",val:10});
    }}>  点击自增  </button>
  • 派发action:使用 store.dispatch 方法派发 action,触发状态更新。

    less 复制代码
    // 分发action
    store.dispatch(addTodo('Learn Redux'));
    store.dispatch(toggleTodo(1));
  • 订阅store:使用 store.subscribe 方法订阅 store 的变化,当状态更新时执行回调函数。

    javascript 复制代码
    // 订阅状态变化
    let unSubscribe = store.subscribe(() => {
      console.log('Current state:', store.getState());
    });
    //函数返回一个方法,调用可以直接解除监听
    unSubscribe();

比较

特性 Vuex Pinia React Context Redux
设计理念 集中式状态管理 轻量级状态管理 组件树值传递机制 可预测状态容器
状态存储 单一 store 多个 store 多个 Context 单一 store
API 复杂度 较高(4个概念) 极低(3个概念) 低(Provider/Consumer) 高(action/reducer/store)
异步处理 Actions 提交 mutations Actions 直接修改状态 需自行实现 需中间件(redux-thunk等)
TypeScript 支持但需额外配置 一流支持 内置支持 需额外类型定义
响应式原理 Vue 响应式系统 Vue 响应式系统 无自动响应,需要手动触发更新 无自动,基于reducer响应
修改数据方式 mutation/action action(可直接修改) setState或回调 dispatch + action
模块化 命名空间 modules 天然多 store 设计 多 Context 隔离 combineReducers
开发体验 模板代码多 简洁直观 简单但功能有限 模板代码多(Redux Toolkit改善)
相关推荐
贵沫末13 分钟前
React——基础
前端·react.js·前端框架
aklry25 分钟前
uniapp三步完成一维码的生成
前端·vue.js
爱学习的茄子33 分钟前
AI驱动的单词学习应用:从图片识别到语音合成的完整实现
前端·深度学习·react.js
10年前端老司机1 小时前
在React项目中如何封装一个可扩展,复用性强的组件
前端·javascript·react.js
sophie旭1 小时前
《深入浅出react开发指南》总结之 10.1 React运行时总览
前端·react.js·源码阅读
轻语呢喃1 小时前
React智能前端:从零开始写的图片分析页面实战
前端·react.js·aigc
MiyueFE1 小时前
每个前端开发者都应该掌握的几个 ReactJS 概念
前端·react.js
用户26124583401612 小时前
vue学习路线(11.watch对比computed)
前端·vue.js
旧时光_2 小时前
Zustand 状态管理库完全指南 - 进阶篇
前端·react.js
阑梦清川3 小时前
Java后端项目前端基础Vue(二)
vue.js