从0到1_前端开发

Web App开发--Vue组件间通信

2017-05-09  本文已影响559人  ef43ffb32440

组件间为什么需要通信


大的Vue项目由组件构成,每个组件维护各自的状态数据。但再完美的架构,也不可能实现组件之间完全解耦。组件之间随时都可能进行数据交互。因此,组件之间通信是不可避免的。

** 构成组件 **
组件意味着协同工作,通常父子组件会是这样的关系:组件A在它的模版中使用了组件B,它们之间必然需要相互通信:父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件。然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。

Vue组件通信的三种方案


Vue的组件间通信分2种情况:父子组件之间通信和非父子组件之间通信。
在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过events 给父组件发送消息。看看它们是怎么工作的。

Vue1.x版本的父子组件之间通信的方式有:Prop传递数据和自定义事件,非父子组件之间通信的方式有中央事件总线。
Vue2.x版本引入状态管理模式的概念,使用专用的状态管理层——Vuex


1. 使用Prop传递数据

组件实例的作用域是孤立的。这意味着不能(也不应该)在子组件的模板内直接引用父组件的数据。要让子组件使用父组件的数据,我们需要通过子组件的props选项。

子组件要显示地用props选项声明它期待获得的数据:

Vue.component('child', {
  // 声明 props
  props: ['message'],
  // 就像 data 一样,prop 可以用在模板内
  // 同样也可以在 vm 实例中像 “this.message” 这样使用
  template: '<span>{{ message }}</span>'
})

然后我们可以这样向它传入一个普通字符串:

<child message="hello!"></child>

结果:

hello!

# camelCase vs. kebab-case
HTML特性是不区分大小写的。所以,当使用的不是字符串模版,camelCased(驼峰式)命名的prop需要转换为相对应的kebab-case(短横线隔开式)命名:

Vue.component('child', {
  // camelCase in JavaScript
  props: ['myMessage'],
  template: '<span>{{ myMessage }}</span>'
})
<!-- kebab-case in HTML -->
<child my-message="hello!"></child>

*如果你使用字符串模版,则没有这些限制。

# 动态Prop
在模板中,要动态地绑定父组件的数据到子模板的props,与绑定到任何普通的HTML特性相类似,就是用 v-bind。每当父组件的数据变化时,该变化也会传导给子组件:

<div>
  <input v-model="parentMsg">
  <br>
  <child v-bind:my-message="parentMsg"></child>
</div>

# 字面量语法 vs 动态语法
初学者常犯的一个错误是使用字面量语法传递数值:

<!-- 传递了一个字符串 "1" -->
<comp some-prop="1"></comp>

因为它是一个字面 prop ,它的值是字符串 "1" 而不是number。如果想传递一个实际的number,需要使用 v-bind ,从而让它的值被当作 JavaScript 表达式计算:

<!-- 传递实际的 number -->
<comp v-bind:some-prop="1"></comp>

# 单向数据流

prop是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态--这会让应用的数据流难以理解。

另外,每次父组件更新时,子组件的所有prop都会更新为最新值。这意味这你不应该在子组件内部改变prop。如果你这么做了,Vue会在控制台给出警告。

为什么我们会有修改prop中数据的冲动呢?通常是这两种原因:

  1. prop 作为初始值传入后,子组件想把它当作局部数据来用;
  2. prop 作为初始值传入,由子组件处理成其它数据输出。

对这两种原因,正确的应对方式是:
1.定义一个局部变量,并用 prop 的值初始化它:

props: ['initialCounter'],
data: function () {
  return { counter: this.initialCounter }
}

2.定义一个计算属性,处理 prop 的值并返回。

props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

<u>*注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态</u>

# Prop验证

我们可以为组件的 props 指定验证规格。如果传入的数据不符合规格,Vue 会发出警告。当组件给其他人使用时,这很有用。

要指定验证规格,需要用对象的形式,而不能用字符串数组:

Vue.component('example', {
  props: {
    // 基础类型检测 (`null` 意思是任何类型都可以)
    propA: Number,
    // 多种类型
    propB: [String, Number],
    // 必传且是字符串
    propC: {
      type: String,
      required: true
    },
    // 数字,有默认值
    propD: {
      type: Number,
      default: 100
    },
    // 数组/对象的默认值应当由一个工厂函数返回
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})

type 可以是下面原生构造器:

type 也可以是一个自定义构造器函数,使用 instanceof 检测。
当 prop 验证失败,Vue会在抛出警告 (如果使用的是开发版本)。

2. 自定义事件

我们知道,父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,应该怎样做?那就是自定义事件!

# 使用 v-on绑定自定义事件

每个 Vue 实例都实现了事件接口(Events interface),即:

Vue的事件系统分离自浏览器的EventTarget API。尽管它们的运行类似,但是$on$emit不是addEventListenerdispatchEvent的别名。

另外,父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。
<u>*不能用$on侦听子组件抛出的事件,而必须在模板里直接用v-on绑定,就像以下的例子:</u>

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
  template: '<button v-on:click="increment">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    increment: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})
new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})

在本例中,子组件已经和它外部完全解耦了。它所做的只是报告自己的内部事件,至于父组件是否关心则与它无关。留意到这一点很重要。

# 给组件绑定原生事件
有时候,你可能想在某个组件的根元素上监听一个原生事件。可以使用 .native 修饰 v-on 。例如:

<my-component v-on:click.native="doTheThing"></my-component>

有时候两个组件也需要通信(非父子关系)。

1. 中央事件总线

在简单的场景下,可以使用一个空的 Vue 实例作为中央事件总线(global event bus)。
比如,假设我们有个 todo 的应用结构如下:

Todos
|-- NewTodoInput
|-- Todo
    |-- DeleteTodoButton

可以通过单独的事件中心管理组件间的通信:

// 将在各处使用该事件中心
// 组件通过它来通信
var eventHub = new Vue()

然后在组件中,可以使用 $emit, $on, $off 分别来分发、监听、取消监听事件:

// NewTodoInput
// ...
methods: {
  addTodo: function () {
    eventHub.$emit('add-todo', { text: this.newTodoText })
    this.newTodoText = ''
  }
}
// DeleteTodoButton
// ...
methods: {
  deleteTodo: function (id) {
    eventHub.$emit('delete-todo', id)
  }
}
// Todos
// ...
created: function () {
  eventHub.$on('add-todo', this.addTodo)
  eventHub.$on('delete-todo', this.deleteTodo)
},
// 最好在组件销毁前
// 清除事件监听
beforeDestroy: function () {
  eventHub.$off('add-todo', this.addTodo)
  eventHub.$off('delete-todo', this.deleteTodo)
},
methods: {
  addTodo: function (newTodo) {
    this.todos.push(newTodo)
  },
  deleteTodo: function (todoId) {
    this.todos = this.todos.filter(function (todo) {
      return todo.id !== todoId
    })
  }
}
2. Vuex

对于模块很多的复杂的情况下,用中央事件总线来实现组件间通信是很困难的。
对于大多数复杂情况,更推荐使用一个专用的状态管理层如:Vuex
由于篇幅较大,Vuex将在下一章中讲解:Web App开发--Vuex状态管理层

<a name="independent-event">无依赖的事件传递机制</a>


这里介绍一种事件传递机制,本人自己实现的。如果项目中已经用到了Vuex,则可以大大减少这个异步事件机制的使用,Vuex使用dispatch实现组件间传递事件来执行事务。但是Vuex是专门用于状态管理的,dispatch的事务最好专注于数据状态的更改,而很多网络请求和业务逻辑最好放到组件的methods中实现。Vuex是不能dispatch组件methods中定义的方法的。而这个事件传递机制可以注册和触发任何组件的任何的方法。
用法很简单:

/**
 * 注册一个事件
 * @param eventName: 字符串,事件的名字
 * @param callback: 事件触发后执行的函数 
**/
register: function(eventName, callback);

/**
 * 注销一个事件
**/
unRegister: function(eventName);

/**
 * 触发一个事件
 * @param eventName: 字符串,事件的名字
 * @param data: 传递的数据,任意js对象 
**/
fire: function(eventName, data);

/**
 * 触发所有注册的事件
**/
fireAll: function ();

/**
 * 判断一个事件是否已经注册
**/
isExisted: function (eventName);

/**
 * 注销所有事件
**/
clear: function ();

下载源码可到:事件传递源码

上一篇: Web App开发--Vue组件化应用构建
下一篇:Web App开发--Vuex状态管理层

上一篇下一篇

猜你喜欢

热点阅读