发表日期:2017-05 文章编辑:小灯 浏览次数:1085
大的Vue项目由组件构成,每个组件维护各自的状态数据。但再完美的架构,也不可能实现组件之间完全解耦。组件之间随时都可能进行数据交互。因此,组件之间通信是不可避免的。
** 构成组件 **
组件意味着协同工作,通常父子组件会是这样的关系:组件A在它的模版中使用了组件B,它们之间必然需要相互通信:父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件。然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。
Vue的组件间通信分2种情况:父子组件之间通信和非父子组件之间通信。
在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过events 给父组件发送消息。看看它们是怎么工作的。
![]()
Vue1.x版本的父子组件之间通信的方式有:Prop传递数据和自定义事件,非父子组件之间通信的方式有中央事件总线。
Vue2.x版本引入状态管理模式的概念,使用专用的状态管理层——Vuex。
组件实例的作用域是孤立的。这意味着不能(也不应该)在子组件的模板内直接引用父组件的数据。要让子组件使用父组件的数据,我们需要通过子组件的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 的值初始化它:
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会在抛出警告 (如果使用的是开发版本)。
我们知道,父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,应该怎样做?那就是自定义事件!
# 使用 v-on绑定自定义事件
每个 Vue 实例都实现了事件接口(Events interface),即:
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> 有时候两个组件也需要通信(非父子关系)。
在简单的场景下,可以使用一个空的 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 }) } } 对于模块很多的复杂的情况下,用中央事件总线来实现组件间通信是很困难的。
对于大多数复杂情况,更推荐使用一个专用的状态管理层如:Vuex 。
由于篇幅较大,Vuex将在下一章中讲解:Web App开发--Vuex状态管理层。
这里介绍一种事件传递机制,本人自己实现的。如果项目中已经用到了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状态管理层