Web App开发--Vue组件间通信
组件间为什么需要通信
大的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中数据的冲动呢?通常是这两种原因:
- prop 作为初始值传入后,子组件想把它当作局部数据来用;
- 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 可以是下面原生构造器:
- String
- Number
- Boolean
- Function
- Object
- Array
type 也可以是一个自定义构造器函数,使用 instanceof 检测。
当 prop 验证失败,Vue会在抛出警告 (如果使用的是开发版本)。
2. 自定义事件
我们知道,父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,应该怎样做?那就是自定义事件!
# 使用 v-on绑定自定义事件
每个 Vue 实例都实现了事件接口(Events interface),即:
- 使用 **$on(eventName) **监听事件
- 使用 **$emit(eventName) **触发事件
Vue的事件系统分离自浏览器的EventTarget API。尽管它们的运行类似,但是$on和 $emit不是addEventListener和 dispatchEvent的别名。
另外,父组件可以在使用子组件的地方直接用 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 ();
下载源码可到:事件传递源码。