Vue状态管理

一、Vuex 介绍

Vuex 是什么?

​ 试想一下,如果在一个项目开发中频繁的使用组件传参的方式来同步data中的值,一旦项目变得很庞大,管理和维护这些值将是相当棘手的工作。为此,Vue为这些被多个组件频繁使用的值提供了一个统一管理的工具——VueX。在具有VueX的Vue项目中,我们只需要把这些值定义在VueX中,即可在整个Vue项目的组件中使用。

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

​ Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

​ 简单来说: 对 vue 应用中多个组件的共享状态进行集中式的管理(读/写)

​ 1) github 站点: https://github.com/vuejs/vuex

​ 2) 在线文档: https://vuex.vuejs.org/zh/

什么是“状态管理模式”?

让我们从一个简单的 Vue 计数应用开始:

<template>
  <div>
    {{ count }}
    <button @click="increment">+</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    increment() {
      this.count++;
    },
  },
};
</script>

<style>
</style>

这个状态自管理应用包含以下几个部分:

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化(包含 n 个更新状态的方法)。

以下是一个表示“单向数据流”理念的简单示意:

在这里插入图片描述

多组件共享状态的问题

但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。

对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。

这就是 Vuex 背后的基本思想,借鉴了 FluxReduxThe Elm Architecture。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。

如果你想交互式地学习 Vuex,可以看这个 Scrimba 上的 Vuex 课程,它将录屏和代码试验场混合在了一起,你可以随时暂停并尝试。

App.vue

<template>
  <div>
    {{ count }}
    <button @click="increment">+</button>
    <hr />
    <Child :count="count" :increment="increment"></Child>
  </div>
</template>

<script>
import Child from "./components/Child";
export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    increment() {
      this.count++;
    },
  },
  components: { Child },
};
</script>

<style>
</style>

Child.vue

<template>
  <div>
    {{ count }}
    <button @click="increment">+</button>
  </div>
</template>

<script>
export default {
  props: ["count", "increment"],
};
</script>

<style >
</style>

什么情况下我应该使用 Vuex?

Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。

如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。

二、安装

直接下载 / CDN 引用

https://unpkg.com/vuex

Unpkg.com 提供了基于 NPM 的 CDN 链接。以上的链接会一直指向 NPM 上发布的最新版本。您也可以通过 https://unpkg.com/vuex@2.0.0 这样的方式指定特定的版本。

在 Vue 之后引入 vuex 会进行自动安装:

<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>

NPM

npm install vuex --save

# 或
yarn add vuex

在一个模块化的打包系统中,您必须显式地通过 Vue.use() 来安装 Vuex:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

当使用全局 script 标签引用 Vuex 时,不需要以上安装过程。

Promise

Vuex 依赖 Promise。如果你支持的浏览器并没有实现 Promise (比如 IE),那么你可以使用一个 polyfill 的库,例如 es6-promise

你可以通过 CDN 将其引入:

<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.js"></script>

然后 window.Promise 会自动可用。

如果你喜欢使用诸如 npm 或 Yarn 等包管理器,可以按照下列方式执行安装:

npm install es6-promise --save # npm
yarn add es6-promise # Yarn

或者更进一步,将下列代码添加到你使用 Vuex 之前的一个地方:

import 'es6-promise/auto'

自己构建

如果需要使用 dev 分支下的最新版本,您可以直接从 GitHub 上克隆代码并自己构建。

git clone https://github.com/vuejs/vuex.git node_modules/vuex
cd node_modules/vuex
npm install
npm run build

三、开始

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:

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

简单的 Store 模式 (不推荐)

安装 Vuex 之后,让我们来创建一个 store。创建过程直截了当——仅需要提供一个初始 state 对象和一些 mutation:

初始化store下index.js中的内容

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  // 可以把这个看成是data,专门用来存储数据
  // 如果在组件中想访问这个数据 $store.state.xxx
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

export default store

现在,你可以通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更:

store.commit('increment')

console.log(store.state.count) // -> 1

为了在 Vue 组件中访问 this.$store property,你需要为 Vue 实例提供创建好的 store。Vuex 提供了一个从根组件向所有子组件,以 store 选项的方式“注入”该 store 的机制:

new Vue({
  el: '#app',
  store: store,
})

现在我们可以从组件的方法提交一个变更:

methods: {
  increment() {
    this.$store.commit('increment')
      // 也可以在组件中直接使用$store.state.count
    console.log(this.$store.state.count)
  }
}

**再次强调,我们通过提交 mutation 的方式,而非直接改变 store.state.count,是因为我们想要更明确地追踪到状态的变化。**这个简单的约定能够让你的意图更加明显,这样你在阅读代码的时候能更容易地解读应用内部的状态改变。此外,这样也让我们有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。

由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。触发变化也仅仅是在组件的 methods 中提交 mutation。

不推荐直接操作state中的数据,因为万一导致了数据紊乱,不能快速定位到错误的原因,因为每个组件都可能有操作数据的方法。

简单计数器

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 100
  },
  mutations: {
    increment(state) {
      state.count++
    }
  }
})

export default store

App.vue

<template>
  <div>
    {{ $store.state.count }}
    <button @click="increment">+</button>
    <hr />
    <Child></Child>
  </div>
</template>

<script>
import Child from "./components/Child";
export default {
  methods: {
    increment() {
      // this.$store.state.count++;
        this.$store.commit("increment");
    },
  },
  components: {
    Child,
  },
};
</script>

<style>
</style>

Child.vue

<template>
  <div>
    {{ $store.state.count }}
    <button @click="increment">+</button>
  </div>
</template>

<script>
export default {
  methods: {
    increment() {
      this.$store.commit("increment");
    },
  },
};
</script>

<style >
</style>

main.js

/*
入口JS
 */
import Vue from 'vue'
import App from './App.vue'
import store from './store/index'

/* eslint-disable no-new */
new Vue({
  el: '#app',
  components: {
    App
  }, // 映射组件标签
  template: '<App/>', // 指定需要渲染到页面的模板
  store
})

四、核心概念

State

Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 ”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

单状态树和模块化并不冲突——在后面的章节里我们会讨论如何将状态和状态变更事件分布到各个子模块中。

  1. vuex 管理的状态对象,可以把这个看成是data,专门用来存储数据

  2. 它应该是唯一的

  3. 如果在组件中想访问这个数据 $store.state.xxx

const state = { 
    xxx: initValue 
}

Mutations

如果要操作store中的state值,只能通过调用mutations提供的方法,才能操作对应的数据,不推荐直接操作state中的数据,因为万一导致了数据紊乱,不能快速定位到错误的原因,因为每个组件都可能有操作数据的方法。

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    // 如果想要调用mutation中的方法,只能使用this.$store.commit("方法名")
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})

你不能直接调用一个 mutation handler。这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。”要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法:

store.commit('increment')
  1. 包含多个直接更新 state 的方法(回调函数)的对象

  2. 谁来触发: action 中的 commit(‘mutation 名称’)

  3. 只能包含同步的代码, 不能写异步代码

const mutations = { 
    yyy (state, {data1}) {
        // 更新 state 的某个属性
    } 
}

注意:在mutations中的方法,最多传递两个参数。其中参数1:是state状态,参数2:通过commit提交过来的参数。如果传递多个参数,可以传递一个对象。例如{a:1,b:3}

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

需要注意的是,在mutations中必须是同步的函数。在 mutation 中混合异步调用会导致你的进程很难调试。例如,当你调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,mutation 都是同步事务:

store.commit('increment')
// 任何由 "increment" 导致的状态变更都应该在此刻完成。

Actions

Action 类似于 mutation,不同在于:

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

让我们来注册一个简单的 action:

const store = new Vuex.Store({
  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 实例本身了。

实践中,我们会经常用到 ES2015 的 参数解构 来简化代码(特别是我们需要调用 commit 很多次的时候):

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}
  1. 包含多个事件回调函数的对象

  2. 通过执行: commit()来触发 mutation 的调用, 间接更新 state

  3. 谁来触发: 组件中: $store.dispatch(‘action 名称’, data1) // ‘zzz’

  4. 可以包含异步代码(定时器, ajax)

const actions = { 
    zzz ({commit, state}, data1) { 
        commit('yyy', {data1}) 
    } 
}

分发 Action

Action 通过 store.dispatch 方法触发:

store.dispatch('increment')

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作

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

调用

export default {
  methods: {
    increment() {
      console.log("dispatch");
      this.$store.dispatch("increment");
    },
  },
};

Getters

getters只负责对外提供数据,不负责修改数据。如果想要修改state中的数据,请去找mutations。

有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数:

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

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

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

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

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})
  1. 包含多个计算属性(get)的对象

  2. 谁来读取: 组件中: $store.getters.xxx

const getters = { 
    mmm (state) { 
        return ...
    } 
}

Modules

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

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

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

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

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
  1. 包含多个 module

  2. 一个 module 是一个 store 的配置对象

  3. 与一个组件(包含有共享数据)对应

Vuex结构图

官方结构图:

在这里插入图片描述

结构图详解:

在这里插入图片描述

Vuex提供了mapState、MapGetters、MapActions、mapMutations等辅助函数给开发在vm中处理store。

五、Vuex应用案例

改版Vue组件化实现的评论页面;所有关于操作comments数据的组件通信都改为Vuex来实现;

目录结构

src
	|- components
		|- Add.vue
		|- Item.vue
		|- List.vue
	|- store
		|- actions.js
		|- getters.js
		|- index.js
		|- mutations.js
		|- state.js
		|- types.js
	App.vue
	main.js

状态管理实现

index.js

/*
    Vuex核心管理模块store对象
*/
import Vue from 'vue'
import Vuex from 'vuex'

import state from './state'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  mutations,
  actions,
  getters
})

actions.js

/*
    包含多个用于间接更新状态的方法的对象模块
*/
import { ADD_COMMENT, DELETE_COMMENT } from './types'

export default {

  addComment ({commit}, comment) {
    // 提交一个comutation请求
    commit(ADD_COMMENT, {comment}) // 传递给mutation的是一个包含数据的对象
  },

  deleteComment ({commit}, index) {
    commit(DELETE_COMMENT, {index})
  }
}

types.js

/* 
    包含多个 mutation name  常量
*/
export const ADD_COMMENT = 'add_comment'
export const DELETE_COMMENT = 'delete_comment'

mutations.js

/* 
    包含多个用于直接更新状态的方法的对象模块
*/
import { ADD_COMMENT, DELETE_COMMENT } from './types'
export default {
  [ADD_COMMENT](state, {comment}) {
    state.comments.unshift(comment)
  },
  [DELETE_COMMENT](state, {index}) {
    state.comments.splice(index, 1)
  },
}

state.js

/*
    状态对象模块
*/
export default {
  comments: [{
      name: "张三",
      content: "Vue 真好用",
    },
    {
      name: "李四",
      content: "Vue 真难啊",
    },
  ]
}

getters.js

/*
    包含多个基于state的getter计算属性方法的对象模块
*/
export default {
  // 总数量
  totalSize(state) {
    return state.commonts.length
  }
}

组件实现

App.vue

<template>
  <div id="app">
    <div>
      <header class="site-header jumbotron">
        <div class="container">
          <div class="row">
            <div class="col-xs-12">
              <h1>请发表对Vue的评论</h1>
            </div>
          </div>
        </div>
      </header>
      <div class="container">
        <Add/>
        <List/>
      </div>
    </div>
  </div>
</template>

<script>
// App组件中引入Add和List
import Add from "./components/Add";
import List from "./components/List";

export default {
  // 映射组件标签
  components: {
    Add,
    List,
  },
};
</script>

<style>
</style>

List.vue

<template>
  <div class="col-md-8">
    <h3 class="reply">评论回复:( {{ totalSize }} )</h3>
    <h2 v-show="comments.length == 0">暂无评论,点击左侧添加评论!!!</h2>
    <ul class="list-group">
      <!-- 遍历comments,并且把每个comments的数据传递给子组件Item  -->
      <Item
        v-for="(comment, index) in comments"
        :key="index"
        :comment="comment"
        :index="index"
      />
    </ul>
  </div>
</template>

<script>
// 在List组件引入Item
import Item from "./Item";
import { mapGetters } from "vuex";
import { mapState } from "vuex";
export default {
  components: {
    Item,
  },
  // 我们一般要获取Vuex管理的数据,推荐在组件中使用计算属性;
  // 当然我们也完全可以使用$store.state.comments
  computed: {
    // 下面的计算属性可以简写为:
    /* 
      ...mapState(['comments'])
    */
    comments() {
      return this.$store.state.comments;
    },
    // 下面的计算属性可以简写为:
    /* 
      ...mapGetters(['totalSize']),
    */
    totalSize() {
      return this.$store.getters.totalSize;
    },
  },
};
</script>

<style>
.reply {
  margin-top: 0px;
}
</style>

Add.vue

<template>
  <div class="col-md-4">
    <form class="form-horizontal">
      <div class="form-group">
        <label>用户名</label>
        <!-- 双向数据绑定 -->
        <input
          type="text"
          class="form-control"
          placeholder="用户名"
          v-model="name"
        />
      </div>
      <div class="form-group">
        <label>评论内容</label>
        <!-- 双向数据绑定 -->
        <textarea
          class="form-control"
          rows="6"
          placeholder="评论内容"
          v-model="content"
        ></textarea>
      </div>
      <div class="form-group">
        <div class="col-sm-offset-2 col-sm-10">
          <!-- 绑定事件 -->
          <button type="button" class="btn btn-default pull-right" @click="add">
            提交
          </button>
        </div>
      </div>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      name: "",
      content: "",
    };
  },
  methods: {
    add() {
      // 1.检查输入的合法性
      const { name, content } = this;
      // 姓名校验
      let name_reg = /^[\u4e00-\u9fa5]{2,4}$/;
      if (!name_reg.test(name)) {
        alert("请输入2-4位中文姓名");
        return;
      }
      if (content.length == "") {
        alert("评论不能为空");
        return;
      }

      // 2.根据输入的数据封装成一个comment对象
      const comment = {
        name,
        content,
      };

      // 3.添加到comments中去,子向父传递数据
      this.$store.dispatch("addComment", comment);

      // 4.清空数据
      this.name = "";
      this.content = "";
    },
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

Item.vue

<template>
  <li class="list-group-item">
    <div class="handle">
      <a href="javascript:;" @click="deleteItem">删除</a>
    </div>
    <p class="user">
      <span>{{ comment.name }}</span
      ><span>说:</span>
    </p>
    <p class="centence">{{ comment.content }}</p>
  </li>
</template>

<script>
export default {
  props: ["comment", "index"],
  methods: {
    deleteItem() {
      const { comment } = this;
      let tf = window.confirm(`确定删除${comment.name}的评论吗?`);
      if (tf) {
        // 触发actions里面的deleteComment
        this.$store.dispatch("deleteComment");
      }
    },
  },
};
</script>

<style>
li {
  transition: 0.5s;
  overflow: hidden;
}

.handle {
  width: 40px;
  border: 1px solid #ccc;
  background: #fff;
  position: absolute;
  right: 10px;
  top: 1px;
  text-align: center;
}

.handle a {
  display: block;
  text-decoration: none;
}

.list-group-item .centence {
  padding: 0px 50px;
}

.user {
  font-size: 22px;
}
</style>

render函数

类型:(createElement: () => VNode) => VNode

字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode。

如果组件是一个函数组件,渲染函数还会接收一个额外的 context 参数,为没有实例的函数组件提供上下文信息。
new Vue({
  // 挂载在index.html的#app元素上
  el: '#app',
  store,
  render: h => h(App)
})

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JTZ001

你的鼓励是我创作的最大动力?

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值