vue组件通信传值——Vuex
一、Vuex介绍
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex 也集成到 Vue 的官方调试工具devtools extension,提供了诸如零配置的 time-tavel 调试、状态快照导入导出等高级调试功能。
1、状态管理模式
一个简单的 Vue 计数应用范例如下所示:
new Vue({ // state data () { return { count: 0 } }, // view template: ` <div>{{ count }}</div> `, // actions methods: { increment () { this.count++ } } })
状态自管理应用包含如下部分:
- state,驱动应用的数据源;
- view,以声明方式将state映射到视图;
- actions,响应在view上的用户输入导致的状态变化;
2、单向数据流理念
用下图表示“单向数据流”理念的简单示意:
但在应用遇到 多组件共享状态 时,单向数据流的简洁性很容易遭到破坏:
(1)问题一:多个视图依赖于同一状态。
传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
(2)问题二:来自不同视图的行为需要变更同一状态。
经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
(3)解决思路
把组件的共享状态抽取出来,以一个全局单例模式管理!!
在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
以上也是Vuex背后的基本思想,借鉴了 Flux、Redux 和 The Elm Architecture。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。
3、Vuex图解
首先通过vue组件触发,比如执行了一个按钮事件,修改一个页面中的数据。然后通过dispatch方法分发到Vuex(类似一个商店,包含有Actions、Mutations、State),将数据共享给Vuex中的State(一定是关联于某个组件的某个标签上);当要更改数据时,先分发到Actions,再提交给Mutations,随后修改State。最后通过Render方法将数据渲染到vue组件中。
需要特别注意:Dispatch这一步是异步的,而Commit这一步是同步的。
4、项目结构
(1)Vuex必须遵循的规则
Vuex不限制代码结构,但规定了一些需要遵守的规则:
1)应用层级的状态应集中到单个 store 对象中;
2)提交 mutation 是更改状态的唯一方法,且这个过程是同步的;
3)异步逻辑都应该封装在 action 里面。
只要遵循上述规则,可以任意组织代码。如果遇到store文件过大的情况,只需将 action、mutation 和 getter 分割到单独的文件。
(2)项目结构示例
对于大型应用,会希望将 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 # 产品模块
二、Vuex实践
1、安装Vuex
(1)直接下载/CDN引用方式
CDN地址:https://unpkg.com/vuex,该链接一直指向NPM上发布的最新版本。
还可以使用 https://unpkg.com/vuex@2.0.0 这样的方式指定特定的版本。
当使用全局script标签引用 vuex 时,会进行自动安装:
<script src="/path/to/vue.js"></script> <script src="/path/to/vuex.js"></script>
(2)NPM安装
npm install vuex --save
在模块化的打包系统中,必须显式地通过 Vue.use()来安装Vuex:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex)
2、vuex-demo项目创建配置
创建测试项目如下所示:
$ vue init webpack vuex-demo ? Project name vuex-demo ? Project description A Vue.js project ? Author xiugeng <443614404@qq.com> ? Vue build standalone ? Install vue-router? No ? Use ESLint to lint your code? No ? Set up unit tests No ? Setup e2e tests with Nightwatch? No ? Should we run `npm install` for you after the project has been created? (recom mended) npm vue-cli · Generated "vuex-demo".
安装vuex组件:
$ cd vuex-demo/ $ npm install vuex -S
3、Vuex应用核心——Store(仓库)
“Store”基本上就是一个容器,它包含着应用中大部分的状态(state)。
(1)Vuex和单纯的全局对象的两点不同
1)Vuex的状态存储是响应式的
当Vue组件从 Store 中读取状态的时候,若 Store 中状态发送变化,那么相应的组件也会相应得到高效更新。
2)不能直接改变 Store 中的状态
改变 Store 中的状态的唯一途径就是下显式地提交(commit)mutation。这样使得可以方便地跟踪每个状态的变化,从而能够通过实现一些工具帮助更好地了解自己的应用。
(2)简单 Store样例
安装 Vuex 后,创建一个 store。创建 src/store 目录,再创建 src/store/index.js 文件,用作组装模块并导出store。
import Vue from 'vue' import Vuex from 'vuex' // 使用插件(如果在模块化构建系统中,请确保在开头调用了 Vue.use(Vuex)) Vue.use(Vuex); const store = new Vuex.Store({ // 五大将:state mutation action getter module state:{ count: 1 }, mutations:{ }, actions:{ } }); export default store;
在 src/main.js 文件中引入Store,将store保存在组件中,共享store状态:
import Vue from 'vue' import App from './App' import store from './store/index' Vue.config.productionTip = false; /* eslint-disable no-new */ new Vue({ el: '#app', store, // store保存在组件中,可共享store状态 components: { App }, template: '<App/>' })
在修改 src/components/HelloWorld.vue,使用computed实时监听状态对象:
<template> <div class="hello"> <h2>{{myCount}}</h2> </div> </template> <script> export default { name: 'HelloWorld', data () { return { msg: 'Welcome to Your Vue.js App' } }, computed: { myCount(){ // 通过 store.state 来获取状态对象 return this.$store.state.count; } } } </script>
由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。
显示效果:
(3)子组件共享store信息
创建组件 src/components/Child.vue,编码内容如下所示:
<template> <div> <p>{{myCount}}</p> </div> </template> <script> export default { name: "Child", data() { return { }; }, computed: { myCount(){ return this.$store.state.count; } } }; </script>
在组件 src/components/HelloWorld.vue 中引入子组件,形成父子关系:
<template> <div class="hello"> <h2>{{myCount}}</h2> <!-- 渲染子组件 --> <Child/> </div> </template> <script> // 引入子组件,形成父子关系 import Child from './Child' export default { name: 'HelloWorld', data () { return { msg: 'Welcome to Your Vue.js App' } }, computed: { myCount(){ // 通过 store.state 来获取状态对象 return this.$store.state.count; } }, components: { Child } } </script>
显示效果如下所示:
三、Vuex——mutation使用
1、 mutation 状态变更
更改 Vuex 的 store 中的状态(state)的唯一方法是提交(commit) mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。
修改 HelloWorld.vue 文件:
<template> <div class="hello"> <h2>{{myCount}}</h2> <!-- 渲染子组件 --> <Child/> <button @click="change">修改</button> </div> </template> <script> // 引入子组件,形成父子关系 import Child from './Child' export default { name: 'HelloWorld', data () { return { msg: 'Welcome to Your Vue.js App' } }, computed: { myCount(){ // 通过 store.state 来获取状态对象 return this.$store.state.count; } }, components: { Child }, methods: { change(){ // 修改状态 this.$store.commit('addCount',3) } } } </script>
再在 store/index.js中声明 mutations 方法 addCount:
const store = new Vuex.Store({ // 五大将:state mutation action getter module state:{ count: 1 }, mutations:{ // 声明方法 // 只能做同步操作,不能直接commit addCount(state, val) { state.count += val; } }, actions:{ } });
显示效果:
点击修改按钮,页面上显示的数字从1》4》7》10》13实时修改变换。
通过提交 mutation 的方式,而非直接改变 store.state.count
,是因为我们想要更明确地追踪到状态的变化。这个简单的约定能够让你的意图更加明显,这样你在阅读代码的时候能更容易地解读应用内部的状态改变。此外,这样也让我们有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。有了它,我们甚至可以实现如时间穿梭般的调试体验。
2、mutation中不能做异步操作
Mutation 重要的原则就是要记住 mutation 必须是同步函数。
每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
(1)在HelloWorld.vue中添加异步操作按钮
<template> <div class="hello"> <h2>{{myCount}}</h2> <!-- 渲染子组件 --> <Child/> <button @click="change">同步修改</button> <button @click="asyncHandler">异步修改</button> </div> </template> <script> // 引入子组件,形成父子关系 import Child from './Child' export default { name: 'HelloWorld', data () { return { msg: 'Welcome to Your Vue.js App' } }, computed: { myCount(){ // 通过 store.state 来获取状态对象 return this.$store.state.count; } }, components: { Child }, methods: { change(){ // 修改状态,更改 Vuex的store中的状态state的唯一方法是提交(commit)mutation this.$store.commit('addCount',3); }, asyncHandler(){ this.$store.commit('asyncHandler',1); } } } </script>
(2)mutation处理提交的事件
在src/store/index.js中添加异步操作:
import Vue from 'vue' import Vuex from 'vuex' // 使用插件(如果在模块化构建系统中,请确保在开头调用了 Vue.use(Vuex)) Vue.use(Vuex); const store = new Vuex.Store({ // 五大将:state mutation action getter module state:{ count: 1 }, mutations:{ // 声明方法 // 只能做同步操作,不能直接commit addCount(state, val) { state.count += val; // 同步操作 }, asyncHandler(state, val) { setTimeout(()=>{ // 异步操作 state.count += val; }, 2000); }, }, actions:{ } }); export default store;
(3)现象展示
1.使用Vue Devtool调试系统:
2.点击同步修改:
3.点击异步修改:
注意:异步操作执行后,页面上的值发生了修改,但却没有修改state中的值。其他组件调用state值时会出现数据对不上。
四、vuex——Action 的使用
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态
- Action 可以包含任意异步操作
1、注册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.state
和 context.getters
来获取 state 和 getters。
实践中,通常使用 ES2015 的参数解构 来简化代码(尤其是需要多次调用 commit 的场景)。
src/store/index.js 修改如下所示:
import Vue from 'vue' import Vuex from 'vuex' // 使用插件(如果在模块化构建系统中,请确保在开头调用了 Vue.use(Vuex)) Vue.use(Vuex); const store = new Vuex.Store({ // 五大将:state mutation action getter module state:{ count: 1 }, mutations:{ // 声明方法 // 只能做同步操作,不能直接commit addCount(state, val) { state.count += val; // 同步操作 }, asyncHandler(state, val) { // 只做同步操作 state.count += val; }, }, actions:{ // Action类似mutation,但Action提交的是mutation,不直接变更状态,且可以包含任意异步操作 addCount({commit},val) { commit('addCount',val); }, asyncHandler({commit},val) { setTimeout(()=>{ commit('asyncHandler', val); },2000) } } }); export default store;
2、分发Action
Action 通过 store..dispatch 方法触发,在组件中使用 this.$store.dispatch('xxx')
分发 action,修改HelloWorld.vue如下所示:
<template> <div class="hello"> <h2>{{myCount}}</h2> <!-- 渲染子组件 --> <Child/> <button @click="change">同步修改</button> <button @click="asyncHandler">异步修改</button> </div> </template> <script> // 引入子组件,形成父子关系 import Child from './Child' export default { name: 'HelloWorld', data () { return { msg: 'Welcome to Your Vue.js App' } }, computed: { myCount(){ // 通过 store.state 来获取状态对象 return this.$store.state.count; } }, components: { Child }, methods: { change(){ // 修改状态,更改 Vuex的store中的状态state的唯一方法是提交(commit)mutation this.$store.dispatch('addCount',3); }, asyncHandler(){ this.$store.dispatch('asyncHandler',1); } } } </script>
异步修改state的值,问题解决,点击两次同步修改和一次异步修改后,显示如下所示:
之所以不直接分发 mutation 而要多这一步操作,主要是因为 mutation必须同步执行这个限制。而Action则没有这个约束,可以在action内部执行异步操作。