通信方式1(单向绑定): Props down, Events up (建议使用)
Props down, Events up 是指 使用props向子组件传递数据,父组件属性发生变化时,子组件可实时更新视图;子组件发生变化,可以使用$emit发送事件消息,以此向父组件传递变化消息。
props 是单向的,当父组件的属性变化时,将传递给子组件,但子组件中的props属性变化不会影响父组件的属性变化(props属性类型是Object除外)。倘若使用vue1.0的.sync强制双向,那么子组件的更改将会影响到父组件的状态,随着业务的增多,很容易让数据流变得难以理解,最终陷入痛苦的泥潭。因此,vue2.0已经剔除.sync,且不允许在子组件中更改自身的props属性。如果真的需要更改props,那一定是设计方案出了问题,请使用替代方案,如:在data选项或computed选项中再定义一个不同的变量进行数据转换。这是props down。
既然父组件可以通过props像子组件传递信息了,那子组件的数据变化如何通知到父组件呢?
$emit的出现便解决了这一问题,该方法用于 子组件向父组件发送事件消息,可带上子组件的数据信息一起发送,父组件接收到消息后,做出自身相应的变更。vue1.0 和vue2.0均支持$emit。这是events up。
如示例代码1,父组件通过 age(props) 向子组件传递数据信息,子组件拿到后,通过$emit向父组件传递最新状态。如果子组件涉及到可能会对age进行更改,则重新定义一个变量age$进行中转。
【示例代码1】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <body> <div id= "app" > <p>parent age: {{age}}</p> <p><button @click= "changeAge" >changeAge to 333</button></p> <hr> <child :age= "age" @on-age-change= "onAgeChange" ></child> </div> <script> Vue.component( 'child' , { template: '<div><p>child age (props选项): {{age}}</p> child age$ (data选项): {{age}}</p> <button @click="changeAge(222)">changeAge to 222</button></div>' , props: { age: Number }, data () { return { age$: this .age } }, methods: { changeAge (age) { this .age$ = age this .$emit( 'on-age-change' , this .age$) } } }); new Vue({ el: '#app' , data: { age: 111 }, methods: { onAgeChange (val) { this .age = val }, changeAge () { this .age = 333 } } }) </script> </body> |
通信方式2(双向绑定): .sync 或 v-model(建议在简单场景中使用)
在复杂逻辑组件中,一定不要使用.sync,很容易在N个组件中绕晕。如图, A 是 BC 的父组件,AB和AC都双向了一个共同的props属性(如:model.sync)。B中的Model变化除了影响父组件A,A的变化进而还会影响组件C,这时C就要爆炸了,这Model变化到底来自A,还是来自B。如此,Model的变化变得很难跟踪,增大维护成本。如果B或C还watch model的话,啊呵,足以毁掉你一天的心情了。
父子组件直接双向绑定是个隐式毒虫,但对于某些基础组件来说却是只有益的蜜蜂,可以省掉不少麻烦。一些简单基础的组件,或不需要关心数据流的地方 使用.sync 或 v-model就会是代码显得简洁,且一目了然。
示例代码2 和 示例代码3 效果图:
vue1.0修饰符.sync可以强制props属性双向绑定,如示例代码2,checked为双向绑定,可以轻松完成radio单选组件。
vue2.0中对prop属性进行直接赋值更改会抛错,但如果prop属性类型为object时,仅仅添加或更改props属性内部的属性不会抛错。由于此特性,vue2.0不支持.sync修饰符。
【示例代码2】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <body> <div id= "app" > <radio v- for = "item in data" :value= "item.value" :checked.sync= "checked" >{{item.text}}</radio> </div> <script> Vue.component( 'radio' , { template: '<label><input type="radio" :value="value" v-model="checked"><slot></slot></label>' , props: { value: {}, checked: {} } }); new Vue({ el: '#app' , data: { checked: '' , data: [ {text: '2G' , value: '2G' }, {text: '3G' , value: '3G' }, {text: '4G' , value: '4G' } ] } }) </script> </body> |
vue2.0虽然已经弃用.sync,但有语法糖v-model,其 实质 就是 props down, events up,只是因为v-model隐藏了数据流,因此唤其为双向绑定。 父组件向下使用props隐式属性value将数据传递数据给子组件,子组件使用$emit('input')向上将数据返回给父组件。但正如上文所说,对于基础组件或不关心数据流的组件使用双向绑定是糟粕中的小蜂蜜,书写简洁,清晰明了。
【示例代码3】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <body> <div id= "app" > <radio v- for = "item in data" :label= "item.value" v-model= "checked" >{{item.text}}</radio> </div> <script> Vue.component( 'radio' , { template: '<label><input type="radio" :value="label" v-model="checked"><slot></slot></label>' , props: { label: {}, value: {} }, computed: { checked: { get () { return this .value }, set (val) { this .$emit( 'input' , val) } } } }); new Vue({ el: '#app' , data: { checked: '' , data: [ {text: '2G' , value: '2G' }, {text: '3G' , value: '3G' }, {text: '4G' , value: '4G' } ] } }) </script> </body> |
拆解语法糖v-model,如示例代码4。已熟知的朋友可以略过。
代码第4行 <child :value="info" @input="dataChange"> </child>更为 <child v-model="info"></child> 效果一样。
【示例代码4】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <body> <div id= "app" > <p>parent info: {{info}}</p> <child :value= "info" @input= "dataChange" ></child> <p><button @click= "changeInfo" >changeInfo from parent</button></p> </div> <script> Vue.component( 'child' , { template: '<div><p>child data: {{data}}</p> <button @click="changeData">changeData from child</button></div>' , props: { value: {} }, computed: { data: { get () { return this .value }, set (val) { this .$emit( 'input' , val) } } }, methods: { changeData () { this .data = 'This is a child component!' } } }); new Vue({ el: '#app' , data: { info: 'This is a original component!' }, methods: { dataChange (info) { this .info = info }, changeInfo () { this .info = 'This is a parent component!' } } }) </script> </body> |
通信方式3: $broadcast 和 $dispatch(不建议使用)
只有vue1.0中才有这两种方法。
$dispatch首先会触发自身实例,冒泡向上,直到某个父组件收到$dispatch消息。如果子组件内部使用了$dispatch,那么该组件的 父组件链在写监听事件时都必须格外小心,必须得有父组件截获该消息,否则会一直冒泡。这是一项非常危险的行为,因为,父组件链中的组件 很难关注到所有子组件的dispatch消息,随着$dispatch在组件中增多,顶层组件或中间组件想知道消息来自哪个子组件变得异常艰辛,事件流跟踪困难,痛苦深渊由此开启。
$broadcast会向每一个子树路径发送消息,一条路径某个组件接收消息,则该路径停止向下发送消息,其它路径规则一样。同理$dispatch,随着通信的增加,消息的增多,子组件也将很难跟踪监听的消息到底来自哪个父组件。不注意的话,最后上演一场的 寻亲记,想来也是持久精彩的。
$dispatch 和 $broadcast 都已在vue2.0中被弃用,如果实在想坚持使用,也可通过$emit进行模拟,通过apply或call改变this,递归便利。
非父子组件间的通信
1. 状态管理方案 vuex(建议使用)
适合复杂数据流管理。详细使用方法见如下站点,
https://github.com/vuejs/vuex
2. 中央数据总线(建议在简单的场景中使用)
创建一个单独的空Vue实例(Events = new Vue()),作为事件收发器,在该实例上进行$emit, $on, $off等操作。适用场景:a. 使用Events.$on的组件不关心事件具体来源; b. 事件处理程序 执行与否 或 重复执行 都没有副作用(如刷新、查询等操作)。如示例代码5,
【示例代码5】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <body> <div id= "app" > <h5>A组件</h5> <a-component></a-component> <h5>B组件</h5> <b-component></b-component> </div> <script> var Events = new Vue() Vue.component( 'AComponent' , { template: '<button @click="changeBName">change B name</button>' , methods: { changeBName () { Events.$emit( 'on-name-change' , 'The name is from A component!' ) } } }); Vue.component( 'BComponent' , { template: '<p>B name: {{name}}</p>' , data () { return { name: 'sheep' } }, created () { Events.$on( 'on-name-change' , (name) => { this .name = name }) } }); new Vue({ el: '#app' }) </script> </body> |
为什么说只适用简单场景呢? vue组件化的开发模式意味着一个组件很可能被多次实例化。请看下文分析,
假设 A组件使用Events.$emit('event'), 在同一个界面被实例化了两次,现在的需求是,组件A实例1触发消息'event'时,组件B根据消息'event'相应更新,组件A实例2触发消息'event',组件B不能根据消息'event'进行相应的更新。这时,因为组件B使用的是Events.$on('event')就搞不清是由A组件实例1触发的消息'event', 还是A组件实例2触发的。
因此,使用该方法时,最好保证组件在同一界面只会被渲染一次,或这不需要关心Events.$emit由哪个实例触发。
3. 利用对象的值传递特性
适用场景:列表中,需要更改某条数据的某项信息。当一个变量向另一个变量复制引用类型的值时,复制的值实际上是一个指针,指向存储在堆中的同一个对象。因此,改变其中一个变量就会影响另一个变量。 如,在一个表格列表中,如何在 N行X列 更改 N行Y列 的数据?
【示例代码6】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | <body> <div id= "app" > <table> <tr> <th v- for = "title in table.head" >{{title}}</th> </tr> <tr v- for = "item in table.data" > <td>{{item.name}}</td> <td><status :value= "item.status" ></status></td> <td><t- switch :item= "item" >切换</t- switch ></td> </tr> </table> </div> <script> Vue.component( 'status' , { template: '<span>{{value}}</span>' , props: { value: {} } }); Vue.component( 't-switch' , { template: '<button @click="switchStatus">切换状态</button>' , props: { item: {} }, methods: { switchStatus () { this .item.status = this .item.status === '有效' ? '无效' : '有效' } } }); new Vue({ el: '#app' , data: { table: { head: [ '广告名称' , '状态' , '操作' ], data: [] } }, ready () { var timer = setTimeout(() => { this .table.data = [ {name: 'adName1' , status: '无效' }, {name: 'adName2' , status: '有效' }, {name: 'adName3' , status: '无效' } ] clearTimeout(timer) }, 1000) } }) </script> </body> |
4. Vue-rx
简而言之,无论是vue1.0,还是vue2.0,为保证清晰的数据流 和 事件流,父子组件通信遵循“props down, events up”的原则。非父子组件根据不同场景选择不同的方案,大部分情况依然建议使用vuex状态管理方案。特别复杂的场景,建议使用vue-rx。