vue进阶-vuex

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

将多个组件共享的变量全部存储在一个对象里面,供所有组件共享,相当于全局变量。而且数据发生改变时,会 响应式 地更新所有组件,相当于订阅模式。

一般情况下,我们会在 Vuex 中存放一些需要在多个界面共享的信息。比如用户的登录状态用户名称头像地理位置信息商品的收藏购物车中的物品等,这些状态信息,我们可以放在统一的地方,对它进行保存和管理。

本章只做学习记录,详尽的内容一定要去官网查看api文档: Vuex

0. 入门

安装

npm install vuex@next --save

vuex示例

1、src下创建一个叫 store 的文件夹,并创建 index.js,创建并暴露一个 store
vue进阶-vuex_第1张图片

import { createStore } from 'vuex'

// 创建一个新的 store 实例
const store = createStore({
    //相当于data,只是它是全局的,所有组件都能访问
    //其实,我们可以通过获取state.count直接修改其属性值
    //但是,state是全局的,多个组件共享的属性,那么对该属性值的操作,就不应该放在具体的组件中
    //从而,引出mutations
    state() {
        return {
            count: 0
        }
    },
    //mutations中编写对共享数据state的修改逻辑
    mutations: {
        increment(state) {
            state.count++
        }
    }
})

export default store

仅需要提供一个初始 state 对象和一些 mutation

2、创建需要的组件

<template>
    this is About!
    <span>这里访问store的count属性:{{ $store.state.count }}</span>
</template>

在 Vue 组件中, 可以通过 this.$store 访问 store 实例。

3、在 main.js 中导入并 use store
vue进阶-vuex_第2张图片

import { createApp } from 'vue'
import App from './App.vue'
import store from '@/store'

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

4、测试

vue进阶-vuex_第3张图片

操作 store 实例属性值

如果需要修改 store.state.count 属性值,有两种方式。

方式一:直接获取 store 实例,并且修改 state 属性值 (不推荐

<template>
    this is About!
    <span>这里访问store的count属性:{{ $store.state.count }}</span>
    <button @click="change"> change </button>
</template>

<script>
export default {
    name: 'About',
    methods: {
        change() {
            this.$store.state.count++;
        }
    }
}
</script>

方式二:通过 mutation

<template>
    this is About!
    <span>这里访问store的count属性:{{ $store.state.count }}</span>
    <button @click="change"> change </button>
</template>

<script>
export default {
    name: 'About',
    methods: {
        change() {
            //this.$store.state.count++;
            this.$store.commit('increment')
        }
    }
}
</script>

vue进阶-vuex_第4张图片

注意:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地 提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
  3. 一般情况,在组件中调用 store 中的状态简单到仅需要在 计算属性 conputed中返回即可;触发变化也仅仅是在组件的 methods 中提交 mutation

1. state

存储在 Vuex 中的数据和 Vue 实例中的 data 遵循相同的规则,由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在 计算属性 computed 中返回某个状态:

<template>
    this is About!
    <span>这里访问store的count属性:{{ getCount }} </span>
</template>

<script>
export default {
    computed: {
        getCount(){
            return this.$store.state.count
        }
    }
}
</script>

每当 this.$store.state.count 变化的时候,都会重新求取计算属性,并且触发更新相关联的 DOM。

mapState 辅助函数

当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性。

<template>
  this is About! <br/>
  <span>这里访问store的count属性:{{ getCount }}</span> <br/> 
  <span>这里访问store的count属性:{{ countAlias }}</span> <br/>
  <span>这里访问store的count属性:{{ countPlusLocalState }}</span>
</template>

<script>
import { mapState } from 'vuex'
export default {
    data(){
        return{
            localCount: 10
        }
    },
    computed: mapState({
        // 箭头函数可使代码更简练
        getCount: state => state.count,

        // 传字符串参数 'count' 等同于 `state => state.count`
        countAlias: 'count',

        // 为了能够使用 `this` 获取局部状态,必须使用常规函数
        countPlusLocalState (state) {
            return state.count + this.localCount
        }
    })
};
</script>

vue进阶-vuex_第5张图片

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组

<template>
  this is About! <br/>
  <span>这里访问store的count属性:{{ count }}</span> <br/> 
</template>

<script>
import { mapState } from 'vuex'
export default {
    data(){
        return{
            localCount: 10
        }
    },
    computed: mapState( ['count'] )
};
</script>

对象展开运算符

mapState 函数返回的是一个 对象。我们如何将它与局部计算属性混合使用呢?通常,我们使用 对象展开运算符 ... 将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。这将极大地简化写法:

<template>
  this is About! <br/>
  <span>这里访问store的count属性:{{ count }}</span> <br/> 
  <span>这里访问组件本地的localCount属性:{{ localComputed }}</span> <br/> 
</template>

<script>
import { mapState } from 'vuex'
export default {
    data(){
        return{
            localCount: 10
        }
    },
    computed: {
        ...mapState( ['count'] ),
        localComputed () {
            return this.localCount + 10
        }
    }
};
</script>

vue进阶-vuex_第6张图片

小结

使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长不直观

如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。

2. getters

有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数,一般做法组件内使用计算属性 computed。

computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}

如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它 —— 无论哪种方式都不是很理想。

Vuex 允许我们在 store 中定义 getter(可以认为是 store 的计算属性)。

Getter 接受 state 作为其第一个参数:

1、store/index.js 中定义 getters

import { createStore } from 'vuex'

// 创建一个新的 store 实例
const store = createStore({
    state: {
      todos: [
        { id: 1, text: '吃饭', done: true },
        { id: 2, text: '工作', done: false }
      ]
    },
    getters: {
      doneTodos (state) {
        return state.todos.filter(todo => todo.done)
      },
      //接受其他 getter 作为第二个参数
      doneTodosCount (state, getters) {
        return getters.doneTodos.length
      }
    }
  })

export default store

通过属性访问

Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]

Getter 也可以接受其他 getter 作为第二个参数:

getters: {
  // ...
  doneTodosCount (state, getters) {
    return getters.doneTodos.length
  }
}
store.getters.doneTodosCount // -> 1

我们可以很容易地在任何组件中使用它:

computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

mapGetters 辅助函数

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。

2、组件使用 store 中的 getter 属性

<template>
  this is About! <br/>
  <span>这里访问store的count属性:{{ count }}</span> <br/> 
  <span>这里访问组件本地的localCount属性:{{ localComputed }}</span> <br/> 
  <span>这里访问store中的 getter 属性:{{ doneTodos }}</span> <br/> 
  <span>这里访问store中的 getter 属性:{{ doneTodosCount }}</span> <br/>
</template>

<script>
import { mapState, mapGetters } from 'vuex'
export default {
    data(){
        return{
            localCount: 10
        }
    },
    computed: {
        ...mapState( ['count'] ),
        ...mapGetters( ['doneTodos', 'doneTodosCount'] ),
        localComputed () {
            return this.localCount + 10
        }
    }
};
</script>

vue进阶-vuex_第7张图片

3. mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。它会接受 state 作为第一个参数:

const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})

组件不能直接调用一个 mutation 处理函数。需要调用 store.commit 方法:

store.commit('increment')

提交载荷(Payload)

你可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload)

// ...
mutations: {
  increment (state, n) {
    state.count += n
  }
}
store.commit('increment', 10)

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

// ...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
store.commit('increment', {
  amount: 10
})

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

store.commit({
  type: 'increment',
  amount: 10
})

使用常量替代 Mutation 事件类型

使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = createStore({
  state: { ... },
  mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能
    // 来使用一个常量作为函数名
    [SOME_MUTATION] (state) {
      // 修改 state
    }
  }
})

Mutation 必须是同步函数

一条重要的原则就是要记住 mutation 必须是同步函数

mapMutations辅助函数

使用 mapMutations 辅助函数将组件中的 methods 映射为 this.$store.commit 调用。

1、store/index.js 中定义 mutations

import { createStore } from 'vuex'

// 创建一个新的 store 实例
const store = createStore({
    state: {
      count: 0
    },
    mutations: {
        increment (state, payload) {
            state.count += payload.count
        }
    }
  })

export default store

注:mutations 内部方法间不能相互调用,可以使用 action 编写调用逻辑,或者组件 methods 内定义方法编写调用逻辑。

2、组件使用 store 中的 mutations属性

<template>
  this is About! <br/>
  <span>这里访问store的count属性:{{ count }}</span> <br/> 
  <button @click="add"> add </button>  <br/> 
  <button @click="increment({count: 20})"> increment </button>
</template>

<script>
import { mapState, mapGetters, mapMutations } from 'vuex'
export default {
    computed: {
         ...mapState( ['count'] )
    },
    methods: {    
        //commit 调用 store 方法
        add() {
            this.$store.commit('increment',  {count: 10})
        },
        //mapMutations映射,相当于将 `this.increment()` 映射为 `this.$store.commit('increment')`,直接调用increment即可
        ...mapMutations( ['increment'] )
    }
};
</script>

vue进阶-vuex_第8张图片

4. action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

相当于 SpringMvc 的 Controller 层,进行 mutation 的调用编排。注册一个简单的 action:

const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.statecontext.getters 来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。

还可以直接解构来简化代码(特别是我们需要调用 commit 很多次的时候):

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

分发 Action

Action 通过 store.dispatch 方法触发:

store.dispatch('increment')

还记得 mutation 必须同步执行 这个限制么?Action 就不受约束!我们可以在 action 内部执行 异步 操作:

1、store/index.js 中定义 actions

import { createStore } from 'vuex'

// 创建一个新的 store 实例
const store = createStore({
    state: {
        count: 0
    },
    mutations: {
        increment(state) {
            state.count++
        },
        increment2(state) {
            state.count++
        }
    },
    actions: {
        increment(context) {
            //1、action 可以组织 mutation 的调用逻辑
            context.commit('increment')
            context.commit('increment2')
        },
        incrementAsync({ commit }) {
            //2、action 内部执行异步操作
            setTimeout(() => {
                commit('increment')
            }, 1000)
        }
    }
})

export default store

2、组件使用 store 中的 actions 属性

<template>
  this is About! <br/>
  <span>这里访问store的count属性:{{ count }}</span> <br/> 
  <button @click="add"> add </button>  <br/> 
  <button @click="add2"> add2 </button>  <br/> 
</template>

<script>
import { mapState, mapGetters, mapMutations } from 'vuex'
export default {
    computed: {
         ...mapState( ['count'] )
    },
    methods: {
        add() {
            this.$store.dispatch('increment')
        },
        add2() {
            this.$store.dispatch('incrementAsync')
        }
    }
};
</script>

vue进阶-vuex_第9张图片

mapActions

mapActions 辅助函数将组件的 methods 映射为 this.$store.dispatch 调用。

<template>
  this is About! <br/>
  <span>这里访问store的count属性:{{ count }}</span> <br/> 
  <button @click="increment"> add </button>  <br/> 
  <button @click="incrementAsync"> add2 </button>  <br/> 
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
    computed: {
         ...mapState( ['count'] )
    },
    methods: {
        ...mapActions( ['increment','incrementAsync'] )
    }
};
</script>

组合 Action

Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise:

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

你可以在组件内调用:

this.$store.dispatch('actionA').then(() => {
  // ...
})

也可以在另外一个 action 中调用:

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

最后,如果我们利用 async / await,我们可以如下组合 action:

// 假设 getData() 和 getOtherData() 返回的是 Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

5. Module

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

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter。

const moduleA = {
  state: () => ({ count: 0 }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

注意: 根模块访问子模块的state,通过store.state.a.count

项目结构

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

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

根级别的 action、mutation 可以访问子模块的 action、mutation。

模块中的状态

1、对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },
  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

2、对于模块内部的 getter,根节点状态会作为第三个参数暴露出来

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

3、对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

你可能感兴趣的:(Vue,vue.js)