创建一个 Vue 实例
每个 Vue 应用都是通过用 Vue
函数创建一个新的 Vue 实例开始的:
var vm = new Vue({// 选项
})
虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm
(ViewModel 的缩写) 这个变量名表示 Vue 实例。
当创建一个 Vue 实例时,你可以传入一个选项对象。这篇教程主要描述的就是如何使用这些选项来创建你想要的行为。作为参考,你也可以在 API 文档中浏览完整的选项列表。
一个 Vue 应用由一个通过 new Vue
创建的根 Vue 实例,以及可选的嵌套的、可复用的组件树组成。举个例子,一个 todo 应用的组件树可以是这样的:
根实例
└─ TodoList├─ TodoItem│ ├─ TodoButtonDelete│ └─ TodoButtonEdit└─ TodoListFooter├─ TodosButtonClear└─ TodoListStatistics
我们会在稍后的组件系统章节具体展开。不过现在,你只需要明白所有的 Vue 组件都是 Vue 实例,并且接受相同的选项对象 (一些根实例特有的选项除外) 。
数据与方法
一个 Vue 实例被创建时,它将 data
对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。
// 我们的数据对象
var data = { a: 1 }// 该对象被加入到一个 Vue 实例中
var vm = new Vue({data: data
})// 获得这个实例上的 property
// 返回源数据中对应的字段
vm.a == data.a // => true// 设置 property 也会影响到原始数据
vm.a = 2
data.a // => 2// ……反之亦然
data.a = 3
vm.a // => 3// 注意只有这些被代理的属性是响应的。如果在实例创建之后添加新的属性到实例上,它不会触发视图更新。
当这些数据改变时,视图会进行重渲染。值得注意的是只有当实例被创建时就已经存在于 data
中的 property 才是响应式的。也就是说如果你添加一个新的 property,比如:
vm.b = 'hi'
那么对 b
的改动将不会触发任何视图的更新。如果你知道你会在晚些时候需要一个 property,但是一开始它为空或不存在,那么你仅需要设置一些初始值。比如:
data: {newTodoText: '',visitCount: 0,hideCompletedTodos: false,todos: [],error: null
}
简单的来讲就是,只有当实例被创建时就已经存在于 data 中的 property 才是响应式的。就是创建时有的数据才会有响应式效果,后面直接强加的property是没用的。如果我们一开始就知道后面需要那些相关数据,在创建Vue之前就要把数据加进去,设置一些初始值即可。
这里唯一的例外是使用 Object.freeze()
,这会阻止修改现有的 property,也意味着响应系统无法再追踪变化。
var obj = {foo: 'bar'
}Object.freeze(obj)new Vue({el: '#app',data: obj
})
<div id="app"><p>{{ foo }}</p><!-- 这里的 `foo` 不会更新! --><button v-on:click="foo = 'baz'">Change it</button>
</div>
除了数据 property,Vue 实例还暴露了一些有用的实例 property 与方法。它们都有前缀 $
,以便与用户定义的 property 区分开来。例如:
var data = { a: 1 }
var vm = new Vue({el: '#example',data: data
})vm.$data === data // => true
vm.$el === document.getElementById('example') // => true// $watch 是一个实例方法
vm.$watch('a', function (newValue, oldValue) {// 这个回调将在 `vm.a` 改变后调用
})
你可以在 API 参考中查阅到完整的实例 property 和方法的列表。
实例生命周期钩子
每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
比如 created
钩子可以用来在一个实例被创建之后执行代码:
new Vue({data: {a: 1},created: function () {// `this` 指向 vm 实例console.log('a is: ' + this.a)}
})
// => "a is: 1"
也有一些其它的钩子,在实例生命周期的不同阶段被调用,如 mounted
、updated
和 destroyed
。生命周期钩子的 this
上下文指向调用它的 Vue 实例。
不要在选项 property 或回调上使用箭头函数,比如 created: () => console.log(this.a)
或 vm.$watch('a', newValue => this.myMethod())
。因为箭头函数并没有 this
,this
会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefined
或 Uncaught TypeError: this.myMethod is not a function
之类的错误。
生命周期图示
下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。
在此步骤页面其实已经可以展示出来了
vue的生命周期及使用场景
1. 创建前:beforeCreate(){}
在 new 一个 vue 实例后,只有一些默认的生命周期钩子和默认事件,其他东西还都没有创建,此时还访问不到data
中的属性以及 methods
中的属性和方法,不能再这个阶段使用 data
中的数据和 methods
中的方法,我们可以在当前生命周期创建一个 loading
事件,在页面加载完成之后将loading
移除。
2. 创建完成:created(){}
created [kriˈeɪtɪd]
data
和 methods
都以将被初始化好了,如果要调用 methods
中的方法,或者操作 data 中的内容,最早可以在这个阶段中操作
当前生命周期执行的时候会遍历data中所有的属性,给每一个属性都添加一个 getter、setter 方法,将data中的属性变成一个 响应式属性 ;当前生命周期执行的时候会遍历 data&&methods 身上所有的属性和方法,将这些属性和方法代理到vue的实例身上,在当前生命周期中我们可以进行 前后端数据的交互 (注:在vue项目中,我们在进行前后端交互时一般不用ajax,因为我们只是想单纯的使用ajax,但是却要引入整个jQuery,在一定程度上降低了性能,再者vue本身设计就是尽量减少DOM元素的操作,但jQuery却是注重DOM元素的操作,这就有点不合理,所以,一般用axios)。
拓展:axios与ajax的区别及优缺点
区别:axios是通过Promise实现对ajax技术的一种封装,就像jquery对ajax的封装一样,简单来说就是ajax技术实现了局部数据的刷新,axios实现了对ajax的封装,axios有的ajax都有,ajax有的axios不一定有,总结一句话就是 axios是ajax,ajax不止axios
ajax:
1、本身是针对MVC编程,不符合前端MVVM的浪潮;
2、基于原生XHR开发,XHR本身的架构不清晰,已经有了fetch的替代方案,jquery整个项目太大,单纯使用ajax却要引入整个jquery非常不合理(采取个性化打包方案又不能享受cdn服务);
3、ajax不支持浏览器的back按钮;
4、安全问题ajax暴露了与服务器交互的细节;
5、对搜索引擎的支持比较弱;
6、破坏程序的异常机制;
7、不容易调试。
axios:
1、从node.js创建http请求;
2、支持Promise API;
3、客户端防止CSRF(网站恶意利用);
4、自动转化json数据;
5、提供了一些并发请求的接口。
小测试
到这里我们还不能操作 dom,在此我们可以做个小测试,给 p 绑定 ref
我们运行会发现,他会报错,告诉你 style 是underfunded
3. 挂载前:beforeMount(){}
mount [maʊnt]
执行这个钩子的时候你在内存中已经编译好了模板,模板与数据进行结合,但是还没有挂载到页面上,因此我们可以在当前生命周期中进行数据的修改。
注意:如果有el则直接用el的挂载点,如果没有则用mount挂载点
4. 挂载完成:mounted(){}
mounted ['maʊntɪd]
执行到这个钩子的时候,表示Vue实例已经初始化完成了。此时组件脱离了创建阶段,进入到了运行阶段。如果我们想要通过插件操作页面上的DOM节点,最早可以在这个阶段中执行
当前生命周期数据和模板进行相结合,并且已经挂载到页面上了,因此我们可以在当前生命周期中获取到真实的DOM元素,还可以在当前生命周期中做方法的实例化,给元素添加一个ref属性,且值必唯一,通过:this.$refs.属性获取DOM元素。
那之前那个小测试来验证
当我们把 this.$refs.p1.style.color = 'red'
放到 mounted 里时,从结果可看出,他是可以操作 dom 节点的
5. 更新前:beforeUpdate(){}
当执行这个钩子的时,页面中的显示的数据还是旧的,data中的数据是更新后的,页面还没有和最新的数据保持同步
当数据发生改变的时候当前生命周期就会执行,因此我们可以通过当前生命周期来检测数据的变化,当前生命周期执行的时候会将更新的数据与模板进行相结合,但是并没有挂载到页面上,因此我们可以在当前生命周期中做更新数据的最后修改。
6. 更新完成:updated(){}
页面显示的数据和data中的数据已经保持同步,都是最新的
数据与模板进行相结合,并且将更新后的数据挂载到了页面上。因此我们可以在当前生命周期中获取到最新的DOM结构(注:在当前生命周期中如果做实例化操作的时候须做条件判断)。
这又是一个小测试
我们可以看到他的先后顺序
7. 销毁前:beforeDestroy(){}
当前生命周期中我们需要做事件的解绑,监听的移除,定时器的清除等操作。
这个时间上所有的 data 和 methods,指令,过滤器。。。。处于可用状态。还没有真正被销毁
8. 销毁完成:destroyed(){}
destroy [dɪ’strɔɪ]
当前生命周期中我们需要做事件的解绑,监听的移除,定时器的清除等操作。(注:被keep-alive包裹的组件,不会被销毁,而是被缓存起来)。
这个时候上所有的data 和 methods ,指令 ,过滤器。。。。。。都是出于不可用状态。组件已经被销毁了
第一次页面加载会触发beforeCreate、created、beforeMount、mounted, mounted说明dom渲染完毕
Vue生命周期在真实场景下的业务应用
created:进行ajax请求异步数据的获取、初始化数据
mounted:挂载元素dom节点的获取
nextTick:针对单一事件更新数据后立即操作dom
updated:任何数据的更新,如果要做统一的业务逻辑处理
watch:监听数据变化,并做相应的处理
template
- template的作用是模板占位符,可帮助我们包裹元素,但在循环过程当中,template不会被渲染到页面上
- template中可以调用简单的可以直接返回的一元运算符,并且能直接返回数据的,也可以调用js的全局方法比如,Date.now(),但是不能调用自己在全局定义的变量
- 如果有template,则会把template里面的内容当作渲染的模板,如果没有template,则会渲染挂载点为 app的模板
Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension (opens new window),提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。
了解Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状 态,并以相应的规则保证状态以一种可预测的方式发生变化
使用Vuex管理数据的好处:
- 能够在 vuex 中集中管理共享的数据,便于开发和后期进行维护
- 能够高效的实现组件之间的数据共享,提高开发效率
- 存储在 vuex 中的数据是响应式的,当数据发生改变时,页面中的数据也会同步更新
什么是“状态管理模式”?
让我们从一个简单的 Vue 计数应用开始:
new Vue({// statedata () {return {count: 0}},// viewtemplate: `<div>{{ count }}</div>`,// actionsmethods: {increment () {this.count++}}
})
这个状态自管理应用包含以下几个部分:
- state,驱动应用的数据源;
- view,以声明方式将 state 映射到视图;
- actions,响应在 view 上的用户输入导致的状态变化。
以下是一个表示“单向数据流”理念的简单示意:
但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态。
对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
这就是 Vuex 背后的基本思想,借鉴了 Flux (opens new window)、Redux (opens new window)和 The Elm Architecture (opens new window)。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。
如果你想交互式地学习 Vuex,可以看这个 Scrimba 上的 Vuex 课程 (opens new window),它将录屏和代码试验场混合在了一起,你可以随时暂停并尝试。
什么情况下我应该使用 Vuex?
Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。
如果您不打算开发 大型单页应用 ,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式 (opens new window)就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是:
Flux 架构就像眼镜:您自会知道什么时候需要它。
简单组件传值
传统组件传值方式及缺点:
传统通过输入输出的方式传值
props:输入
$emit:输出
输入输出机制的缺点:页面复杂时,经常需要层层传递数据,因为非父子组件间的交互,只能寻找最近的祖先组件来做中转
同时,输入输出机制还有一个局限性:当不同页面的组件需要进行数据交互时,它就无能为力了。平常开发,这种输入输出的方案也基本满足了
但如果有需要跨页面的数据交互或者说,有需要数据做持久化处理的场景时;以及如果页面组件层次复杂,存在 props 和 $emit 层层传递时。这种方案其实是不好的。
计数器案例
下列代码演示一个计数器案例,此案例演示的时兄弟组件传值的问题
初始化项目
直接使用脚手架创建项目即可,安装的时候可以选择使用 vuex,也可以以后再安装
为了更加明了,我们先创建一个不带 vuex的项目 vuex-demo
以传统方式操作公共数据
增加页面(Addtion.vue):
<template><div><p>当前count变量的值是:{{ count }}</p><button @click="add">+1</button></div>
</template>
<script>
export default {props: {count: Number},methods: {add () {this.$emit('addCount')}}
}
</script>
减少页面(Subtraction.vue):
<template><div><p>当前count变量的值是:{{ count }}</p><button @click="sub">-1</button></div>
</template>
<script>
export default {props: {count: Number},methods: {sub () {this.$emit('subCount')}}
}
</script>
app.vue(公共文件)
<template><div id="app"><my-add :count="count" @addCount="handleChangeAdd"></my-add><my-sub :count="count" @subCount="handleChangeSub"></my-sub></div>
</template><script>
import Addtion from './components/Addtion.vue'
import Subtraction from './components/Subtraction.vue'export default {data () {return {count: 10}},name: 'App',components: {'my-add': Addtion,'my-sub': Subtraction},methods: {handleChangeAdd () {this.count++},handleChangeSub () {this.count--}}
}
</script>
使用 vuex 改造
创建 store 文件夹,在其中创建index.js 用来作为 vuex的仓库
首先安装 vuex
npm install vuex --save
对其进行配置
在使用vuex的方法时,我们可以不用再main.js对其进行操作,我们把它放在 store 文件夹下index.js里
使用vuex里面的方法进行改造
增加页面(Addtion.vue):
<template><div><p>当前count变量的值是:{{ $store.state.count }}</p><button @click="add">+1</button></div>
</template>
<script>
export default {methods: {add () {this.$store.commit('addCount')}}
}
</script>
减少页面(Subtraction.vue):
<template><div><p>当前count变量的值是:{{ $store.state.count }}</p><button @click="sub">-1</button></div>
</template>
<script>
export default {methods: {sub () {this.$store.commit('subCount')}}
}
</script>
app.vue(公共文件)
<template><div id="app"><my-add :count="count"></my-add><my-sub :count="count"></my-sub></div>
</template><script>
import Addtion from './components/Addtion.vue'
import Subtraction from './components/Subtraction.vue'export default {data () {return {count: 10}},name: 'App',components: {'my-add': Addtion,'my-sub': Subtraction}
}
</script>
store文件夹下的 index.js文件
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)const store = new Vuex.Store({state: {count: 10},mutations: {addCount (state) {state.count++},subCount (state) {state.count--}}
})
export default store
安装
直接下载 / CDN 引用
https://unpkg.com/vuex(opens new window)
Unpkg.com (opens new window)提供了基于 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
yarn add vuex
在一个模块化的打包系统中,您必须显式地通过 Vue.use()
来安装 Vuex:
import Vue from 'vue'
import Vuex from 'vuex'Vue.use(Vuex)
当使用全局 script 标签引用 Vuex 时,不需要以上安装过程。
Promise
Vuex 依赖 Promise (opens new window)。如果你支持的浏览器并没有实现 Promise (比如 IE),那么你可以使用一个 polyfill 的库,例如 es6-promise (opens new window)。
你可以通过 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,把状态拿到应用外部管理,Vuex 管这个管理状态的玩意叫 Store,一个完全独立的应用,他只负责状态管理。尝试把 Vuex 应用和 Vue 应用划清界限,
一个 Vuex 应用,做状态管理,可以理解是 Model 层一个 Vue 应用,仅负责数据展示,纯纯的 View 层
Vuex 应用
所谓状态管理,无非就是定义状态,修改状态。
开始
在 Scrimba 上尝试这节课
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
Store
Store与全局对象的区别
- vuex的状态存储是响应式的。当vue组件从store中读取状态时,若store中的状态发生变化,那么相应的组件也会得到更新。
- 不能直接改变store中的状态。改变store中状态的唯一途径就是显式地提交mutation。
Store中state的获取和修改
- 可以通过直接调用
store.state
来获取。 - 只能通过
store.commit
来提交mutation的方式修改store中的state值。
State
定义 state
在 Vuex 里定义状态,我们需要 new 一个 Store 出来,每一个 Vuex 应用的核心就是 store(仓库)。
// store.jsimport Vue from "vue";import Vuex from "vuex";
Vue.use(Vuex);
const store = new Vuex.Store({ state: { count: 0 }});
export default store;
以上代码创建了一个 store,store.state
里定义了状态。与在 Vue 里定义 data 没有任何区别,
下面修改状态。
直接修改
如果你想快点用上 Vuex,你可以在组件里直接修改 store 里的 state,(直接修改的意思就是,用点操作修改)
state.count = 2;
虽然这样可以正常工作,但在严格模式下会报错,更改 store 中的状态的唯一方法应该是提交 mutation。
严格模式
开启严格模式,仅需在创建 store 的时候传入 strict: true
:
const store = new Vuex.Store({ strict: true});
在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。
单一状态树
vuex使用单一状态树,即用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源”而存在。这也就意味着每个应用将仅仅包含一个store实例。
在vue组件中获取vuex状态
最简单的做法就是在计算属性中返回某个状态:
return store.state.count
但是这种做法的缺点就是:在模块化的构建系统中,在每个需要使用state的组件中需要频繁地导入。为此,Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)
):
const app = new Vue({el: '#app',// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件store,components: { Counter },template: `<div class="app"><counter></counter></div>`
})
通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store
访问到。
mapState辅助函数
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用mapState
辅助函数帮助我们生成计算属性:
/ 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'export default {// ...computed: mapState({// 箭头函数可使代码更简练count: state => state.count,// 传字符串参数 'count' 等同于 `state => state.count`countAlias: 'count',// 为了能够使用 `this` 获取局部状态,必须使用常规函数countPlusLocalState (state) {return state.count + this.localCount}})
}
对象展开运算符
mapState
函数返回的是一个对象。要使它能与其他的局部计算属性混合使用,需要用到对象展开运算符(...
):
computed: {localComputed () { /* ... */ },// 使用对象展开运算符将此对象混入到外部对象中...mapState({// ...})
}
Getter
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)}}
})
可以使用store.getters
来调用getter。
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
getters: {// ...getTodoById: (state) => (id) => {return state.todos.find(todo => todo.id === id)}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
mapGetters 辅助函数
mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性,与mapState用法类似。
Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
在Vuex.Store的构造器选项中,有一个 mutation 选项,这个选项就像是事件注册:key 是一个字符串表示 mutation 的类型(type),value 是一个回调函数(handler),
这个回调函数就是我们实际进行状态更改的地方,它有两个入参,第一个参数是 state,就是 store 里的 state;第二个参数是 Payload,这是提交 mutation 时候额外传入的参数。
要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法:
store.commit('increment')
补充 :commit 参数是对象
当 commit 的参数是一个对象的时候,对象里必须要有 type 字段,除了 type 字段,你还可以添加额外任意字段为载荷(payload)。
// 对象风格的提交方式
store.commit({ type: "increment",amount: 10
});
type 做第一参数
把 type 和 payload 分开也是个不错的选择,可以把 type 单独拿出来当第一个参数,以commit(type,[payload])
的形式提交,官方称此为以载荷形式提交。
store.commit("increment");
store.commit("increment", 10);
store.commit("increment", { count: 2 });
在大多数情况下,payload 应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读。
到这里我们已经知道如何定义 mutation 和提交 mutation 了,commit 接口很简单,但在哪里使用呢?有两个地方用
Vue 应用的组件里,在组件里调用store.commit
。还有 store 的 action 里。
提交载荷(Payload)
你可以向store.commit
传入额外的参数,即 mutation 的 载荷(payload),在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读。
对象风格的提交方式
提交 mutation 的另一种方式是直接使用包含 type 属性的对象:
store.commit({type: 'increment',amount: 10
})
Mutation 需遵守 Vue 的响应规则
Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:
- 最好提前在你的 store 中初始化好所有所需属性。
- 当需要在对象上添加新属性时,你应该
- 使用
Vue.set(obj, 'newProp', 123)
, 或者 - 以新对象替换老对象。例如,利用 stage-3 的对象展开运算符我们可以这样写:
- 使用
state.obj = { ...state.obj, newProp: 123 }
Mutation 必须是同步函数
一条重要的原则就是要记住 mutation 必须是同步函数。如果使用的是异步函数,那么任何在回调函数中进行的的状态的改变都是不可追踪的。
在组件中提交 Mutation
你可以在组件中使用this.$store.commit('xxx')
提交 mutation,或者使用mapMutations
辅助函数将组件中的 methods 映射为store.commit
调用(需要在根节点注入store
)。
import { mapMutations } from 'vuex'export default {// ...methods: {...mapMutations(['increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`// `mapMutations` 也支持载荷:'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`]),...mapMutations({add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`})}
}
Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- 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 很多次的时候):
actions: {increment ({ commit }) {commit('increment')}
}
上面的例子向函数传入一个用{}
包围的对象,在ES6语法中将会进行解构,能够直接在函数体中使用对象属性名。
同步和异步
在 Vuex 中,mutation 都是同步事务,在 mutation 中混合异步调用会导致你的程序很难调试。
例如,当你调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。
Action 确实和 mutation 很类似,不同在于:
Action 提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步操作。
注册 action:
注册 action 就跟定义 mutation 一样,除了 handler 的入参不同。
Action 函数的入参是一个与 store 实例具有相同方法和属性的 context
对象。因此可以
context.commit
提交一个 mutation,context.state
获取 state。context.getters
获取 getters。
const store = new Vuex.Store({ state: {count: 0 }, mutations: { increment(state) { state.count++; } }, actions: { increment(context) {context.commit("increment"); } }
});
分发Action
Action 通过store.dispatch
方法触发:
store.dispatch('increment')
组合Action
Action 通常是异步的,那么如何知道 action 什么时候结束呢?首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且store.dispatch
仍旧返回 Promise:
actions: {actionA ({ commit }) {return new Promise((resolve, reject) => {setTimeout(() => {commit('someMutation')resolve()}, 1000)})}
}
Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,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 的状态
项目结构
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
- 应用层级的状态应该集中到单个 store 对象中。
- 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
- 异步逻辑都应该封装到 action 里面。
下面是项目结构示例:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store├── index.js # 我们组装模块并导出 store 的地方├── actions.js # 根级别的 action├── mutations.js # 根级别的 mutation└── modules├── cart.js # 购物车模块└── products.js # 产品模块