vuex基础
vuex基础,教程和解释
注: 本文写成后,Vuex的api发生了巨大的改变,但是基本的理念并没有改变。本文阐述了vuex为何重要,如何工作的,以及它怎样使开发维护更容易
Vuex是Vue.js的作者写的一个原型库,旨在帮助开发者用更可维护的方式开发大型web应用。它遵循与Flux,Redux相似的原则。
与其直接介绍如何使用vuex,不如先解释一下它优于其他方式的基本原理以及它怎样帮助你
我们在做什么?
一个简单的app,仅有一个按钮和一个计数器。按下按钮后,计数器叠加。尽管很简单,但目的是理解底层的概念。
这个app有两个组件:
- 按钮(事件源)
- 计数器(须基于事件更新数值)
这两个组件彼此不能感知到对方,并且不能相互通信。web应用里通常是这个模式,即使是很小的app。在大型应用中,成百上千的组件相互通信, 并且需要不断检查其他组件。例如一个todo list的应用:
本文的目的
探索四种方式解决相同的问题:
- 利用组件间的事件传播
- 利用共享的状态对象
- 利用vuex
读完本文后,希望可以理解:
- 在项目中利用vuex的工作流
- vuex 解决了什么问题
- vuex为何比其他方法好,尽管它显得更啰嗦和严格
建立起点
创建项目
$ npm install -g vue-cli
$ vue init webpack vuex-tutorial
$ cd vuex-tutorial
$ npm install
$ npm install --save vuex
$ npm run dev
首先,创建IncrementButton组件
<template>
<button @click.prevent="activate">+1</button>
</template>
<script>
export default {
methods: {
activate () {
console.log('+1 Pressed')
}
}
}
</script>
<style>
</style>
CounterDisplay组件://filename:src/components/CounterDisplay.vue
<template>
Count is {{ count }}
</template>
<script>
export default {
data () {
return {
count: 0
}
}
}
</script>
<style>
</style>
App.vue:
<template>
<div id="app">
<h3>Increment:</h3>
<increment></increment>
<h3>Counter:</h3>
<counter></counter>
</div>
</template>
<script>
import Counter from './components/CounterDisplay.vue'
import Increment from './components/IncrementButton.vue'
export default {
components: {
Counter,
Increment
}
}
</script>
<style>
</style>
app的原型已经基本出来了,点击按钮只会在控制台打印。
方法1:事件广播
修改 IncrementButton.vue,
export default {
methods: {
activate () {
// Send an event upwards to be picked up by App
this.$dispatch('button-pressed')
}
}
}
app.vue中接受“button-pressed”事件,并向CounterDisplay广播:
export default {
components: {
Counter,
Increment
},
events: {
'button-pressed': function () {
// Send a message to all children
this.$broadcast('increment')
}
}
}
在CounterDisplay中,处理来自父元素的事件:
export default {
data () {
return {
count: 0
}
},
events: {
increment () {
this.count ++
}
}
}
此方法的缺点:
这种写法在技术上虽然没有错误,你也可以把你所有的逻辑都写在一个文件中。但是这并不容易后期维护:
- 对每一个动作,都需要链接到父元素,并且分发事件到相应的组件
- 当应用规模逐渐扩大后,很难理解每个事件的来源究竟是哪个组件
- 没有清晰地方来存放的业务逻辑.
this.count++在CounterDisplay中,但是业务逻辑可能在任何地方,这使得围护变得困难。
让我来举个例子来说明这种方法可能引起的bug:
- 你招聘了两个实习生,A,B。你让A写了另一个计数器组件。让B写了一个归零按钮。
- A写了一个
FormattedCounterDisplay组件,这个组件订阅了increment事件,A愉快地提交了代码。 - B写了归零组件,向app发射
reset事件,app接到事件后重新分发它。他在CounterDisplay实现了响应的代码(使计数器归零),但B不知道A的组件也应订阅了归零事件。 - 你按+1 这没问题,但是按归零后,只有原来的计数器组件归零了。
这是个很简单的例子,但是却表明了状态和业务逻辑的分散性,基于事件分发的方法可能会导致错误。
解法2: 共享状态
重新开始。新建一个store.js
export default {
state: {
counter: 0
}
}
首先修改CounterDisplay.vue:
<template>
Count is {{ sharedState.counter }}
</template>
<script>
import store from '../store'
export default {
data () {
return {
sharedState: store.state
}
}
}
</script>
我们做了几点有趣的事:
- 获得的store对象是个常量对象,定义在其他文件中
- 自己组件的data属性指向
store.state - 因为data是组件的一部分,vue使它成为响应式的,即store.state发生改变后,vue会自动更新
shredState
修改IncrementButton.vue
import store from '../store'
export default {
data () {
return {
sharedState: store.state
}
},
methods: {
activate () {
this.sharedState.counter += 1
}
}
}
这里,导入了store,并且订阅了响应式的state,就像上面的一样。当activate函数调用时,sharedState更改导致store.state更改,使计数器加一。所有订阅了counter的组件和计算属性会更新。
比方法一好在哪里?
仍考虑2个实习生的问题。
- A写了
FormattedComponentDisplay组件,订阅了共享的状态,计数器会一直显示最新的值。 - B的重置组件设置共享状态计数器到0.会影响
CounterDisplay和A写的FormattedComponentDisplay - reset 会像人们期望的一样工作
这种方法的不足之处
- 在他们实习期间,AB会写各式各样的计数器展示组件和重置组件,以及递增组件。它们都修改同一个共享对象,很不错
- 一旦他们离开,你需要维护
- 新来的产品狗C说,不想要计数器超过100
你会怎么做?
- 你会去这些组件中找到更新值的地方,很无聊
- 你会去找展示这些值的地方,增加一个过滤器或格式化器,同样很无聊
问题就在这,业务逻辑分散在应用的各个地方,原则上是很简单的事情,但是很难维护和调试。
稍微好一点的方法
store.js
var store = {
state: {
counter: 0
},
increment: function () {
if (store.state.counter < 100) {
store.state.counter += 1;
}
},
reset: function () {
store.state.counter = 0;
}
}
export default store
这使得代码更干净了,因为你显式调用了increment方法,你所有的业务逻辑都在store里。但是,新实习生不知道这背后的道理,并且发现写store.state.counter更简单,于是会在项目的其他地方修改,于是代码变得更难维护了。
于是你制定了更严格的规定,指南,代码审查来保证没人会在store.js外用其他方式修改state,一旦有人违反,则重罚。
方法3:vuex
原则上 vuex有点像在方法2中做的事情。下面是比较繁杂的原理图
首先创建store.js
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
var store = new Vuex.Store({
state: {
counter: 0
},
mutations: {
INCREMENT (state) {
state.counter ++
}
}
})
export default store
这段代码做了什么?
- 导入vuex,并使其作为vue的插件
- store不再是一个简单的对象,而是Vuex.Stored 实例
- 设置state的counter为0
- 设置了Mutations对象,拥有
INCREMENT方法,接受一个state参数,并改变该state
这段代码做了以下有趣的事:
1.所有 require或import store from './store.js'都会应用同一个store实例,
2. 我们从来不会修改store.state.counter,但我们得到state的一个备份,在这个备份中修改和更新state,这很重要
IncrementButton.vue 这样写
import store from '../store'
export default {
methods: {
activate () {
store.dispatch('INCREMENT')
}
}
}
这个组件没有data,但是点击时直接调用 store.dispatch('INCREMENT'),稍后再说
CounterDisplay.vue文件如下
<template>
Count is {{ counter }}
</template>
<script>
import store from '../store'
export default {
computed: {
counter () {
return store.state.counter
}
}
}
</script>
令人兴奋的是,我们不再订阅任何共享对象了,相反,我们应用vue的计算属性来获取store里的counter值。 Vue会智能地识别出计算属性counter依赖于store.state.counter,所以当store更新后,会自动更新相关的组件,就这么简单。
下面是顺序发生的事情:
- vue的事件处理器调用active方法,这个函数调用了
store.dispatch('INCREMENT') - INCREMENT是个action的id,代表了改变state的类型,可以向dispatch函数传递额外的参数。
- vue会找出对应的mutator,来响应这个dispatch,这里仅仅有一个,当然可以有多个。
- mutator接受state对象的备份,vue会保存旧的state备份,可以用作更高级的用法
- 当state更新后,vue自动更新所有依赖次state的组件。
- 这使得代码更加可测试
比方法2更好地地方
- 若开发过程中所有的state都被保存,开发者可以做出“时间旅行调试器”,除了听起来像个超级英雄,它可以让你插销动作,改变逻辑,开发更快。
- 你可以制作中间件,记录所有state的改变。如,制作一个记录器,记录用户所有的动作,如果找到一个bug,你可以找到那条日志,重放所有的动作,有效地重现bug
- 强制所有的action写在用一个地方,团队里的所有成员就有了一个很好的参考,就可以用所有的改变state的方法了
还有很长的路要走
本文只是介绍了vuex能力的冰山一角。vuex自身仍然在开发,但我确定若干年后它会逐渐成熟,成为开发规范
题外话: 处理实习生的代码
你把vuex用到你的项目里,但是你的实习生仍然觉得直接修改store.state.counter更方便。你可以在你的store.js里加上一行,
var store = new Vuex.Store({
state: {
counter: 0
},
mutations: {
INCREMENT (state) {
state.counter ++
}
},
strict: true // Vuex's patent pending anti-intern device
})
这样直接修改store.state便不会生效。注意这样会使你的代码运行更慢,生产环境可以移除它。