跳至主要內容

10:VueX

三思原创大约 18 分钟前端vue前端vuejs

VueX是专为Vue.js应用程序开发的状态管理模式,提供集中式存储管理应用的所有组件状态。它能够实现多个组件间共享状态,管理用户登录状态、商品信息等。通过state、view和actions实现单界面和多界面的状态管理,将共享状态交给Vuex的全局单例模式进行统一管理,并在组件中访问和修改状态。

image
image

Vue自学笔记10:VueX

本文章为观看B站codewhy老师教学视频自学vuejs过程中记录的笔记,对视频进行了清晰的整理。
(帮大家节省自己整理笔记的时间啦,剩下的时间一定要去上机实操,多多练习!!)
ps:最全最新Vue、Vuejs教程,从入门到精通_哔哩哔哩_bilibiliopen in new window(附上视频链接!)

九、Vuex

1. Vuex的定义

1.1 Vuex是做什么的?

官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。

  • 它采用 集中式存储管理 应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  • Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

状态管理到底是什么?

  • 状态管理模式、集中式存储管理这些名词听起来就非常高大上,让人捉摸不透。
  • 其实,你可以简单的将其看成把需要多个组件共享的变量全部存储在一个对象里面。
  • 然后,将这个对象放在顶层的Vue实例中,让其他组件可以使用。
  • 那么,多个组件是不是就可以共享这个对象中的所有变量属性了呢?

等等,如果是这样的话,为什么官方还要专门出一个插件Vuex呢?难道我们不能自己封装一个对象来管理吗?

  • 当然可以,只是我们要先想想VueJS带给我们最大的便利是什么呢?没错,就是响应式。
  • 如果你自己封装实现一个对象能不能保证它里面所有的属性做到响应式呢?当然也可以,只是自己封装可能稍微麻烦一些。
  • 不用怀疑,Vuex就是为了提供这样一个在多个组件间共享状态的插件,用它就可以了。

1.2 管理什么状态呢?

  1. 如果你做过大型开放,你一定遇到过多个状态,在多个界面间的共享问题。
  2. 比如用户的登录状态(token)、用户名称头像地理位置信息等等。
  3. 比如商品的收藏、购物车中的物品等等。
  4. 这些状态信息,我们都可以放在统一的地方,对它进行保存和管理,而且它们还是响应式的(待会儿我们就可以看到代码了,莫着急)。

单界面的状态管理

  • State:不用多说,就是我们的状态。(你姑且可以当做就是data中的属性)
  • View:视图层,可以针对State的变化,显示不同的信息。(这个好理解吧?)
  • Actions:这里的Actions主要是用户的各种操作:点击、输入等等,会导致状态的改变。

单界面状态管理的实现

在这个案例中,我们有木有状态需要管理呢?没错,就是个数counter。

  • counter需要某种方式被记录下来,也就是我们的State。
  • counter目前的值需要被显示在界面中,也就是我们的View部分。
  • 界面发生某些操作时(我们这里是用户的点击,也可以是用户的input),需要去更新状态,也就是我们的Actions

多界面状态管理 Vue已经帮我们做好了单个界面的状态管理,但是如果是多个界面呢?

  • 多个试图都依赖同一个状态(一个状态改了,多个界面需要进行更新)
  • 不同界面的Actions都想修改同一个状态(Home.vue需要修改,Profile.vue也需要修改这个状态)

也就是说对于某些状态(状态1/状态2/状态3)来说只属于我们某一个试图,但是也有一些状态(状态a/状态b/状态c)属于多个试图共同想要维护的

  • 状态1/状态2/状态3你放在自己的房间中,你自己管理自己用,没问题。
  • 但是状态a/状态b/状态c我们希望交给一个大管家来统一帮助我们管理!!!
  • 没错,Vuex就是为我们提供这个大管家的工具。

全局单例模式(大管家)

  • 我们现在要做的就是将共享的状态抽取出来,交给我们的大管家,统一进行管理。
  • 之后,你们每个试图,按照我规定好的规定,进行访问和修改等操作。
  • 这就是Vuex背后的基本思想。
  • Vuex状态管理图例

2. Vuex简单实用

2.1 简单的案例

我们还是实现一下之前简单的案例

首先,我们需要在某个地方存放我们的Vuex代码:

  • 这里,我们先创建一个文件夹store,并且在其中创建一个index.js文件
  • 在index.js文件中写入如下代码:

挂载到Vue实例中

  • 来到main.js文件,导入store对象,并且放在new Vue中
  • 这样,在其他Vue组件中,我们就可以通过this.$store的方式,获取到这个store对象了

使用Vuex的count

我们来对使用步骤,做一个简单的小节:

  1. 提取出一个公共的store对象,用于保存在多个组件中共享的状态
  2. 将store对象放置在new Vue对象中,这样可以保证在所有的组件中都可以使用到
  3. 在其他组件中使用store对象中保存的状态即可 通过this.$store.state​.属性的方式来访问状态 通过this.$store.commit('mutation中方法')​来修改状态

注意事项:

  • 我们通过提交mutation的方式,而非直接改变store.state.count。这是因为Vuex可以更明确的追踪状态的变化,所以不要直接改变store.state.count的值。

3.Vuex核心概念

Vuex有几个比较核心的概念:

  • State
  • Getters
  • Mutation
  • Action
  • Module

3.0 视屏中代码总结+分析

该部分是所有vuex内容汇总,建议从3.1开始一步步学的时候参照着看.

代码部分:

main.js文件:

 import Vue from 'vue'
 import App from './App'
 import router from './router'
 import store from './store'
 
 Vue.config.productionTip = false
 // Vue.prototype.$store = store //挂载是内部其实会执行这行代码
 
 new Vue({
   el: '#app',
   router,
   store,
   render: h => h(App)
 })
   

App.vue文件:

 <template>
   <div id="app">
     <h2>---------App内容---------</h2>
     <h2>{{ $store.state.counter }}</h2>
     <button @click="add">+</button>
     <button @click="sub">-</button>
     <button @click="addCount(5)">+5</button>
     <button @click="addCount(10)">+10</button>
     <button @click="addStudent">添加学生</button>
     <h2>---------Home内容---------</h2>
     <home />
     <h2>---------Mutation响应规则---------</h2>
     <h2>{{ $store.state.info }}</h2>
     <button @click="addAddress">添加地址</button>
     <h2>---------getters中的方法的使用---------</h2>
     <h2>年龄大于30岁的学生有:{{ $store.getters.ageOlderThan30 }}</h2>
     <h2>年龄大于30岁的学生个数为:{{ $store.getters.numOfAgeOlderThan30 }}</h2>
     <h2>
       年龄大于x岁的学生个数为(x为20):{{ $store.getters.numOfAgeOlderThanx(20) }}
     </h2>
     <h2>---------actions的使用---------</h2>
     <h2>{{ $store.state.info }}</h2>
     <button @click="asyncAddWeight">异步添加体重</button>
     <h2>---------modules的使用---------</h2>
     <h2>{{$store.state.a.name}}</h2>  <!--不一样了:因为a模块会自动加到state中,所以state一定要在a前面 -->
     <button @click="updateName">更新name</button><!--模块的mutations的使用形式不变-->
     <h2>{{$store.getters.fullName1}}</h2><!--模块的getters的使用形式不变-->
     <h2>{{$store.getters.fullName2}}</h2><!--模块的getters的使用形式不变-->
     <h2>{{$store.getters.fullName3}}</h2><!--模块的getters的使用形式不变-->
     <button @click="asyncUpdateName">异步更新name</button>
   </div>
 </template>
 
 <script>
 import Home from "./components/Home.vue";
 export default {
   name: "App",
   components: { Home },
   methods: {
     //普通的提交风格
     add() {
       this.$store.commit("increment");
     },
     sub() {
       this.$store.commit("decrement");
     },
     //传递一个参数
     addCount(count) {
       //payload:负载
       this.$store.commit("incrementCount", count);
     },
     //传递多个参数时,通过传递对象实现
     addStudent() {
       let stu = { name: "jordan", age: 45 };
       this.$store.commit("addStudent", stu);
     },
 
     //特殊的提交风格 (以addCount举例)
     //优点:这种风格可以将传递一个参数和传递多个参数的提交风格统一了,也可以理解成传递多个参数更方便了
     // addCount(count){
     //   let x = 3
     //   this.$store.commit({type:'incrementCount',count,x})
     // }
     //------------------------------------------------------------------------------
     //Mutation响应规则
     addAddress() {
       this.$store.commit({ type: "addAddress" });
     },
     //Actions的使用
     //index.js中Actions中是普通写法时:
     // asyncAddWeight() {
     //   this.$store.dispatch("asyncAddWeight", {
     //     massage: 120,
     //     success: () => {     //回调函数
     //       console.log("异步添加体重成功");
     //     },
     //   }); //可以将message(待传数据)和success(异步操作成功的回调函数)封装在一个对象中
     // },
     //(推荐使用)index.js中Actions中是Promise写法时:
     asyncAddWeight() {
       this.$store    //因为dispatch调用了asyncAddWeight函数,返回的是一个Promise对象,所以后面跟.then函数没毛病
         .dispatch({ type: "asyncAddWeight", weight: 120 }) 
         .then((res) => {    //回调函数
           console.log("异步添加体重成功");
           console.log(res);
         });
     },
     //modules的mutations的使用
     updateName(){   
       this.$store.commit({type:"updateName"})
       //如果store中根store的mutations中和其模块的mutations中都有叫updateName的方法,则都会执行
     },
     asyncUpdateName(){
       this.$store.dispatch("asyncUpdateName")
     }
   }
 };
 </script>
 
 <style>
 </style>
 

index.js文件:

 import Vue from 'vue'
 import Vuex from 'vuex'
 
 Vue.use(Vuex)
 
 const moduleA = {
     state:{
         name:'我是moduleA中的state的name'
     },
     mutations:{
         updateName(state){
             state.name = '我是moduleA中mutations中updateName修改后的name'
         }
     },
     getters:{
         fullName1(state){
             return '我是moduleA中getters中的fullName1'
         },
         fullName2(state,getters){
             return getters.fullName1+'+2'
         },
         fullName3(state,getters,rootState){ //rootState是"根"store中的state对象!!
             //括号里getters参数不能少写!!!
             console.log(rootState);
             return "模块中可以用rootState获取根store中的state对象,如拿rootState的counter值:"+rootState.counter
         }
     },
     actions:{
         asyncUpdateName(context){
             // console.log(context);
             //打印context会发现:其有commit,dispatch方法,和getters,rootGetters,rootState,state对象
             setTimeout(()=>{
                 context.commit('updateName')
             },1000)
         }
     }
 }
 
 const store = new Vuex.Store({
     state: {
         counter: 1000,
         student: [{ name: 'curry', age: 34 }, { name: 'james', age: 38 }, { name: 'pull', age: 23 }],
         info: {
             name: 'double_up',
             age: 20,
             height: 180
         }
     },
     mutations: {
         //普通的提交风格
         increment(state) {
             state.counter++
         },
         decrement(state) {
             state.counter--
         },
         incrementCount(state, count) {
             state.counter += count;
         },
         addStudent(state, stu) {
             state.student.push(stu)
         },
 
         //特殊的提交风格,方法如下写:其实就是将提交的参数封装到到对象中了,将这个参数命名为payload
         // incrementCount(state, payload) {
         //     state.counter += payload.count+payload.x
         // }
         //--------------------------------------------------
         addAddress(state) {
             // state.info["address"]='china' //该方法在vue3中好像是可以响应式的,但vue2中不行,以后试试
             // state.info中响应式添加
             Vue.set(state.info, 'address', 'china')
 
             // state.info中响应式删除则可以使用delete
             // Vue.delete(state.info,'height')
         },
         addWeight(state, weight) {  //这个就是asyncAddWeight中context去commit调用的函数
             Vue.set(state.info, 'weight', weight)
         }
     },
     getters: {
         //返回学生年龄大于30岁的学生
         ageOlderThan30(state) {
             return state.student.filter(s => s.age > 30)
         },
         //获取年龄大于30岁的学生的个数
         //还可以传getters做参数,调用getters里的方法
         numOfAgeOlderThan30(state, getters) {
             //上面的state参数还是要保留的,因为在下方调用ageOlderThan30函数时,还是隐式需要state的
             return getters.ageOlderThan30.length
         },
         //如果需要返回年龄大于x的学生(x为调用时传进来的参数)
         numOfAgeOlderThanx(state) {
             return x => {
                 return state.student.filter(s => s.age > x)
             }
         }
     },
     actions: {
         //actions中的context相当于store对象
         // 普通写法:进行异步处理(网络处理+回调函数)
         // asyncAddWeight(context,payload){
         //     setTimeout(()=>{
         //         context.commit('addWeight',payload.message)
         //         // console.log(payload)
         //         payload.success() //网络处理成功,进行success回调
         //     },1000)
         // }
 
         //(推荐使用)Promise对象写法
         asyncAddWeight(context, payload) {
             console.log(context);
             //打印context会发现:其有commit,dispatch方法,和getters,rootGetters,rootState,state对象
             return new Promise((resolve,reject)=>{
                 setTimeout(() => {
                     context.commit('addWeight', payload.weight)  //在这里commit哦
                     resolve('浅给.then()传个res参数吧')//网络处理成功后,进行.then()的调用
                 }, 1000)
             })
         }
     },
     modules: {
         a : moduleA//moduleA对象定义在store上方了
     }
 })
 
 export default store

分析部分:

index.js文件:

state:

mutations:

getters:

actions:

modules(模块):

模块store中只有获取state状态时和根store不一样外,其他都一样

为什么不一样,为什么一样:

模块store会被放进根store中;

模块store的state会放进根store的state的a对象中,所以调用时要在$store.state后加上.a;

而模块store的getters,mutations则会直接和根store混在一起

3.1 State单一状态树

总结:

vue建议我们不要创建多个store对象,最好只建立一个store对象

3.2 Getters基本使用

总结: 有点类似于计算属性

有时候,我们需要从store中获取一些state变异后的状态,比如下面的Store中:

获取学生年龄大于20的个数。

我们可以在Store中定义getters

Getters作为参数和传递参数 如果我们已经有了一个获取所有年龄大于20岁学生列表的getters, 那么代码可以这样来写

getters默认是不能传递参数的, 如果希望传递参数, 那么只能让getters本身返回另一个函数. 比如上面的案例中,我们希望根据ID获取用户的信息

3.3 Mutation状态更新

总结:

  1. 修改sotre里面的state一定是通过提交(commit)Mutation的方式
  2. 不经过mutations的话,不会被devtools监听到, 所以,所有state的数据更改,都必须经过mutations
  3. Vuex要求我们Mutation中的方法必须是同步方法.而异步方法放到Actions中去.

Vuex的store状态的更新唯一方式:提交Mutation Mutation主要包括两部分:

  • 字符串的事件类型(type)
  • 一个回调函数(handler),该回调函数的第一个参数就是state。

mutation的定义方式:

通过mutation更新

Mutation传递参数

在通过mutation更新数据的时候, 有可能我们希望携带一些额外的参数 参数被称为是mutation的载荷(Payload) Mutation中的代码:

多参数传递

这个时候, 我们通常会以对象的形式传递, 也就是payload是一个对象. 这个时候可以再从对象中取出相关的信息.

Mutation提交风格

Vue还提供了另外一种风格, 它是一个包含type属性的对象

Mutation中的处理方式是将整个commit的对象作为payload使用, 所以代码没有改变, 依然如下:

Mutation响应规则

Vuex的store中的state是响应式的, 当state中的数据发生改变时, Vue组件会自动更新.这就要求我们必须遵守一些Vuex对应的规则:

  • 提前在store中初始化好所需的属性.
  • 当给state中的对象添加新属性时, 使用下面的方式: 方式一: 使用Vue.set(obj, 'newProp', 123)​ 方式二: 用新对象给旧对象重新赋值

当我们点击更新信息时, 界面并没有发生对应改变

Vue.set()在二.6.4也讲过

Mutation常量类型 – 概念

不用也没事

我们来考虑下面的问题:

  • 在mutation中, 我们定义了很多事件类型(也就是其中的方法名称).
  • 当我们的项目增大时, Vuex管理的状态越来越多, 需要更新状态的情况越来越多, 那么意味着Mutation中的方法越来越多.
  • 方法过多, 使用者需要花费大量的经历去记住这些方法, 甚至是多个文件间来回切换, 查看方法名称, 甚至如果不是复制的时候, 可能还会出现写错的情况.

如何避免上述的问题呢?

  • 在各种Flux实现中, 一种很常见的方案就是使用常量替代Mutation事件的类型.
  • 我们可以将这些常量放在一个单独的文件中, 方便管理以及让整个app所有的事件类型一目了然.

具体怎么做呢?

  • 我们可以创建一个文件: mutation-types.js, 并且在其中定义我们的常量.
  • 定义常量时, 我们可以使用ES2015中的风格, 使用一个常量来作为函数的名称.

Mutation同步函数
通常情况下, Vuex要求我们Mutation中的方法必须是同步方法.

  • 主要的原因是当我们使用devtools时, 可以devtools可以帮助我们捕捉mutation的快照.
  • 但是如果是异步操作, 那么devtools将不能很好的追踪这个操作什么时候会被完成. 比如我们之前的代码, 当执行更新时, devtools中会有如下信息:

但是, 如果Vuex中的代码, 我们使用了异步函数:

你会发现state中的info数据一直没有被改变, 因为他无法追踪到. So, 通常情况下, 不要再mutation中进行异步的操作

3.4 Action

总结:

Action类似于Mutation, 但是是用来代替Mutation进行异步操作的.

我们强调, 不要再Mutation中进行异步操作.

  • 但是某些情况, 我们确实希望在Vuex中进行一些异步操作, 比如网络请求, 必然是异步的. 这个时候怎么处理呢?
  • Action类似于Mutation, 但是是用来代替Mutation进行异步操作的.

Action的基本使用代码如下:

context是和store对象具有相同方法和属性的对象. 也就是说, 我们可以通过context去进行commit相关的操作, 也可以获取context.state等. 但是注意, 这里它们并不是同一个对象

Action的分发

在Vue组件中, 如果我们调用action中的方法, 那么就需要使用dispatch

同样的, 也是支持传递payload

Action返回的Promise

在Action中, 我们可以将异步操作放在一个Promise中, 并且在成功或者失败后, 调用对应的resolve或reject.

3.5 Module

总结:

  • Vue使用单一状态树,那么也意味着很多状态都会交给Vuex来管理.
  • 当应用变得非常复杂时,store对象就有可能变得相当臃肿.
  • 为了解决这个问题, Vuex允许我们将store分割成模块(Module), >- 而每个模块拥有自己的state、mutations、actions、getters等

Module局部状态

我们在moduleA中添加state、mutations、getters

亲测,重名的话,两个都执行

mutation和getters接收的第一个参数是局部状态对象

注意: 虽然, 我们的doubleCount和increment都是定义在对象内部的. 但是在调用的时候, 依然是通过this.$store来直接调用的.

Actions的写法

actions的写法呢? 接收一个context参数对象 局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

如果getters中也需要使用全局的状态, 可以接受更多的参数

4. 项目结构

总结:

就是将index.js中的store对象的mutations,getters,actions,modules对象进行模块化抽离,而state则保留在index.js文件中

当我们的Vuex帮助我们管理过多的内容时, 好的项目结构可以让我们的代码更加清晰.

发布于 2022-10-23 17:48