tech_post icon indicating copy to clipboard operation
tech_post copied to clipboard

vuex基础

Open Alexis374 opened this issue 9 years ago • 0 comments

vuex基础,教程和解释


注: 本文写成后,Vuex的api发生了巨大的改变,但是基本的理念并没有改变。本文阐述了vuex为何重要,如何工作的,以及它怎样使开发维护更容易

Vuex是Vue.js的作者写的一个原型库,旨在帮助开发者用更可维护的方式开发大型web应用。它遵循与Flux,Redux相似的原则。

与其直接介绍如何使用vuex,不如先解释一下它优于其他方式的基本原理以及它怎样帮助你

我们在做什么?

一个简单的app,仅有一个按钮和一个计数器。按下按钮后,计数器叠加。尽管很简单,但目的是理解底层的概念。

这个app有两个组件:

  1. 按钮(事件源)
  2. 计数器(须基于事件更新数值)

这两个组件彼此不能感知到对方,并且不能相互通信。web应用里通常是这个模式,即使是很小的app。在大型应用中,成百上千的组件相互通信, 并且需要不断检查其他组件。例如一个todo list的应用:

本文的目的

探索四种方式解决相同的问题:

  1. 利用组件间的事件传播
  2. 利用共享的状态对象
  3. 利用vuex

读完本文后,希望可以理解:

  1. 在项目中利用vuex的工作流
  2. vuex 解决了什么问题
  3. 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 ++
    }
  }
}

此方法的缺点:

这种写法在技术上虽然没有错误,你也可以把你所有的逻辑都写在一个文件中。但是这并不容易后期维护:

  1. 对每一个动作,都需要链接到父元素,并且分发事件到相应的组件
  2. 当应用规模逐渐扩大后,很难理解每个事件的来源究竟是哪个组件
  3. 没有清晰地方来存放的业务逻辑.this.count++CounterDisplay中,但是业务逻辑可能在任何地方,这使得围护变得困难。

让我来举个例子来说明这种方法可能引起的bug:

  1. 你招聘了两个实习生,A,B。你让A写了另一个计数器组件。让B写了一个归零按钮。
  2. A写了一个FormattedCounterDisplay组件,这个组件订阅了increment事件,A愉快地提交了代码。
  3. B写了归零组件,向app发射reset事件,app接到事件后重新分发它。他在CounterDisplay实现了响应的代码(使计数器归零),但B不知道A的组件也应订阅了归零事件。
  4. 你按+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>

我们做了几点有趣的事:

  1. 获得的store对象是个常量对象,定义在其他文件中
  2. 自己组件的data属性指向store.state
  3. 因为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个实习生的问题。

  1. A写了FormattedComponentDisplay组件,订阅了共享的状态,计数器会一直显示最新的值。
  2. B的重置组件设置共享状态计数器到0.会影响CounterDisplay和A写的FormattedComponentDisplay
  3. reset 会像人们期望的一样工作

这种方法的不足之处

  1. 在他们实习期间,AB会写各式各样的计数器展示组件和重置组件,以及递增组件。它们都修改同一个共享对象,很不错
  2. 一旦他们离开,你需要维护
  3. 新来的产品狗C说,不想要计数器超过100

你会怎么做?

  1. 你会去这些组件中找到更新值的地方,很无聊
  2. 你会去找展示这些值的地方,增加一个过滤器或格式化器,同样很无聊

问题就在这,业务逻辑分散在应用的各个地方,原则上是很简单的事情,但是很难维护和调试。

稍微好一点的方法

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

这段代码做了什么?

  1. 导入vuex,并使其作为vue的插件
  2. store不再是一个简单的对象,而是Vuex.Stored 实例
  3. 设置state的counter为0
  4. 设置了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更新后,会自动更新相关的组件,就这么简单。

下面是顺序发生的事情:

  1. vue的事件处理器调用active方法,这个函数调用了store.dispatch('INCREMENT')
  2. INCREMENT是个action的id,代表了改变state的类型,可以向dispatch函数传递额外的参数。
  3. vue会找出对应的mutator,来响应这个dispatch,这里仅仅有一个,当然可以有多个。
  4. mutator接受state对象的备份,vue会保存旧的state备份,可以用作更高级的用法
  5. 当state更新后,vue自动更新所有依赖次state的组件。
  6. 这使得代码更加可测试

比方法2更好地地方

  1. 若开发过程中所有的state都被保存,开发者可以做出“时间旅行调试器”,除了听起来像个超级英雄,它可以让你插销动作,改变逻辑,开发更快。
  2. 你可以制作中间件,记录所有state的改变。如,制作一个记录器,记录用户所有的动作,如果找到一个bug,你可以找到那条日志,重放所有的动作,有效地重现bug
  3. 强制所有的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便不会生效。注意这样会使你的代码运行更慢,生产环境可以移除它。

Alexis374 avatar Apr 15 '16 11:04 Alexis374