数据驱动和组件化是 Vue 最核心的两个功能。
组件化开发给我们带来了可快的开发效率,更好的可维护性。
每个组件都有自己的状态、视图和行为等组成部分。

  1. new Vue({
  2. // state
  3. data() {
  4. return {
  5. count: 0
  6. }
  7. },
  8. // view
  9. template: `
  10. <div>{{ count }}</div>
  11. `,
  12. // actions
  13. methods: {
  14. increment() {
  15. this.count++
  16. }
  17. }

状态管理

状态管理包含以下几部分:

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

组件内状态管理流程 - 图1

组件间通信方式回顾

大多数场景下的组件都并不是独立存在的,而是相互协作共同构成了一个复杂的业务功能。在 Vue 中为不同的组件关系提供了不同的通信规则。
image.png

父组件给子组件传值

父组件通过相应属性给子组件传值,子组件通过 props 接收数据。props 可以是数组,也可以是对象。属性可以只规定属性名,也可以加上数据类型限制,还可以规定默认值。

子组件:

  1. <template>
  2. <div>
  3. <h1>Props Down Child</h1>
  4. <h2>{{ title }}</h2>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. // props: ['title'],
  10. props: {
  11. title: String
  12. // title: {
  13. // type: String,
  14. // default: ''
  15. // }
  16. }
  17. }
  18. </script>

父组件:

  1. <template>
  2. <div>
  3. <h1>Props Down Parent</h1>
  4. <child title="My journey with Vue"></child>
  5. </div>
  6. </template>
  7. <script>
  8. import child from './01-Child'
  9. export default {
  10. components: {
  11. child
  12. }
  13. }
  14. </script>

子组件给父组件传值

子组件给父组件传值需要通过自定义事件。

父组件:

  1. <template>
  2. <div>
  3. <h1 :style="{ fontSize: hFontSize + 'em'}">Event Up Parent</h1>
  4. 这里的文字不需要变化
  5. <child :fontSize="hFontSize" v-on:enlargeText="enlargeText"></child>
  6. <child :fontSize="hFontSize" v-on:enlargeText="hFontSize += $event"></child>
  7. </div>
  8. </template>
  9. <script>
  10. import child from './02-Child'
  11. export default {
  12. components: {
  13. child
  14. },
  15. data () {
  16. return {
  17. hFontSize: 1
  18. }
  19. },
  20. methods: {
  21. enlargeText (size) {
  22. this.hFontSize += size
  23. }
  24. }
  25. }
  26. </script>

子组件:

  1. <template>
  2. <div>
  3. <h1 :style="{ fontSize: fontSize + 'em' }">Props Down Child</h1>
  4. <button @click="handler">文字增大</button>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. props: {
  10. fontSize: Number
  11. },
  12. methods: {
  13. handler () {
  14. this.$emit('enlargeText', 0.1)
  15. }
  16. }
  17. }
  18. </script>

不相关组件通信

不相关组件通信也是通过自定义事件,但是由于组件不相关,所以需要一个公共的 Vue 实例,来作为事件中心,为组件提供$on$emit方法。

事件中心很简单,只需要创建一个 Vue 实例并导出。

  1. // eventbus.js
  2. import Vue from 'vue'
  3. export default new Vue()

需要通信的组件导入 Vue 实例,并在上面注册和触发事件。

传值的组件:

  1. <template>
  2. <div>
  3. <h1>Event Bus Sibling02</h1>
  4. <div>{{ msg }}</div>
  5. </div>
  6. </template>
  7. <script>
  8. import bus from './eventbus'
  9. export default {
  10. data () {
  11. return {
  12. msg: ''
  13. }
  14. },
  15. created () {
  16. bus.$on('numchange', (value) => {
  17. this.msg = `您选择了${value}件商品`
  18. })
  19. }
  20. }
  21. </script>

接收数据的组件:

  1. <template>
  2. <div>
  3. <h1>Event Bus Sibling01</h1>
  4. <div class="number" @click="sub">-</div>
  5. <input type="text" style="width: 30px; text-align: center" :value="value">
  6. <div class="number" @click="add">+</div>
  7. </div>
  8. </template>
  9. <script>
  10. import bus from './eventbus'
  11. export default {
  12. props: {
  13. num: Number
  14. },
  15. created () {
  16. this.value = this.num
  17. },
  18. data () {
  19. return {
  20. value: -1
  21. }
  22. },
  23. methods: {
  24. sub () {
  25. if (this.value > 1) {
  26. this.value--
  27. bus.$emit('numchange', this.value)
  28. }
  29. },
  30. add () {
  31. this.value++
  32. bus.$emit('numchange', this.value)
  33. }
  34. }
  35. }
  36. </script>
  37. <style>
  38. .number {
  39. display: inline-block;
  40. cursor: pointer;
  41. width: 20px;
  42. text-align: center;
  43. }
  44. </style>

$refs 通信

这种方式不推荐使用。
我们可以在 html 标签或组件标签上挂载 ref 属性。挂载在 html 标签上,获取到的是 DOM 元素;挂载在组件标签上,获取到的是组件实例。通过$refs属性可以获取到 ref 属性。被挂载ref的,可以被访问到属性和方法。

子组件:

  1. <template>
  2. <div>
  3. <h1>ref Child</h1>
  4. <input ref="input" type="text" v-model="value">
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. data () {
  10. return {
  11. value: ''
  12. }
  13. },
  14. methods: {
  15. focus () {
  16. this.$refs.input.focus()
  17. }
  18. }
  19. }
  20. </script>

父组件:

  1. <template>
  2. <div>
  3. <h1>ref Parent</h1>
  4. <child ref="c"></child>
  5. </div>
  6. </template>
  7. <script>
  8. import child from './04-Child'
  9. export default {
  10. components: {
  11. child
  12. },
  13. mounted () {
  14. this.$refs.c.focus()
  15. this.$refs.c.value = 'hello input'
  16. }
  17. }
  18. </script>

简易的状态管理方案

如果多个组件之间要共享状态(数据),使用上面的方式虽然可以实现,但是比较麻烦,而且多个组件之间互相传值很难跟踪数据的变化,如果出现问题很难定位问题。
当遇到多个组件需要共享状态的时候,典型的场景:购物车。我们如果使用上述的方案都不合适,我们 会遇到以下的问题:

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

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
我们可以把多个组件的状态,或者整个程序的状态放到一个集中的位置存储,并且可以检测到数据的更改。

以一种简单的方式来实现:

首先创建一个共享的仓库 store 对象:

  1. // store.js
  2. export default {
  3. debug: true,
  4. state: {
  5. user: {
  6. name: 'xiaomao',
  7. age: 18,
  8. sex: '男'
  9. }
  10. },
  11. setUserNameAction (name) {
  12. if (this.debug) {
  13. console.log('setUserNameAction triggered:', name)
  14. }
  15. this.state.user.name = name
  16. }
  17. }

把共享的仓库 store 对象,存储到需要共享状态的组件的 data 中:

  1. <template>
  2. <div>
  3. <h1>componentA</h1>
  4. user name: {{ sharedState.user.name }}
  5. <button @click="change">Change Info</button>
  6. </div>
  7. </template>
  8. <script>
  9. import store from './store'
  10. export default {
  11. methods: {
  12. change () {
  13. store.setUserNameAction('componentA')
  14. }
  15. },
  16. data () {
  17. return {
  18. privateState: {},
  19. sharedState: store.state
  20. }
  21. }
  22. }
  23. </script>

接着我们继续延伸约定,组件不允许直接变更属于 store 对象的 state,而应执行 action 来分发 (dispatch) 事件通知 store 去改变,这样最终的样子跟 Vuex 的结构就类似了。这样约定的好处是,我们能够记录所有 store 中发生的 state 变更,同时实现能做到记录变更、保存状态快照、历史回滚/时光旅行的先进的调试工具。