最近用Vue+Element做一个后台系统,在组件通信方面遇到很多问题。
其实官方文档中关于组件通信,其实已经讲的很详尽了,但内容比较分散,在此总结、复习一下。
单文件组件(SFC)
本文内容基于Vue的单文件组件,也就是.vue文件,官方文档:
单文件组件
组件概念
组件化开发是Vue的核心思想之一,也是Vue官方最推荐的开发范式。
所谓组件化开发,就是把页面中,具有一定独立性的各个区块或功能,拆分成单个组件进行编码维护。
收益和成本
组件化开发的收益,两方面:
- 组件复用:封装一次,到处使用
- 可维护性:单个组件逻辑简单,方便排错、更新
而成本方面,主要就是组件间的通信,也就是本文的主题。
组件级别
从项目结构来看,组件分为三级:
- 页面级组件:最终面向用户展示的单个页面
- 模块级组件:页面中具有特定功能的某个模块,例如:数据列表、工具栏…
- 控件级组件:模块进一步拆分的组件,例如:表格、输入框、按钮…
不同级别的组件,具有各自的功能和特性,高一级的组件包含低一级的组件,呈树形结构:
1 2 3 4 5 6 7 8 9 10
| page: { module1: { controlA, controlB, }, module2: { controlC, controlD, }, }
|
父子组件通信
父attribute -> 子prop
father通过title属性,向son组件传值
father.vue1 2 3 4 5 6 7 8 9 10 11 12
| <template> <son title="儿子你好"></son> </template>
<script> import Son from './son';
export default { name: 'Father', components: { Son }, } </script>
|
子接收title并进行展示
son.vue1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <template> <h3> {{ title }} </h3> </template>
<script> export default { name: 'Son', props: { title: { type: String, default: ()=> '一级标题', validator(val) { return val.length < 100; }, } }, } </script>
|
注意:
- 父的attribute,是
kebaba-case
,短横线分隔,例如big-title
- 子的prop,是
camelCase
,除了第一个单词,其他单词首字母大写,例如bigTitle
title="儿子你好"
,这种方式只能传递字符串
- 传值是数字、变量或表达式,必须用
v-bind:title
或:title
- 子可以对prop设置类型、默认值和校验函数,提高程序健壮性
子$emit(‘event’,value)->父@event
子通过$emit事件(‘hungry’),向父传递值(‘水果’)
son.vue1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <h3 @click="play"> 我是儿子 </h3> </template>
<script> export default { name: 'Son', methods: { play() { console.log('玩球'); console.log('看书'); console.log('学走路'); this.$emit('hungry', '水果'); }, }, } </script>
|
父接收hungry事件,并执行喂食方法
father.vue1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <son @hungry="feed"></son> </template>
<script> import Son from './son';
export default { name: 'Father', components: { Son }, methods: { feed(food) { console.log('喂食'+food); }, }, } </script>
|
注意:
- event的名称,是
kebaba-case
,短横线分隔,例如very-hungry
- 子$emit(‘event’, value),value可以是字面量、变量、表达式等
- 父@event=”handler”,handler为事件处理方法,接收子传递的value参数
父v-model <-> 子value
父通过v-model,向子传递money,实现双向绑定
父可以通过buy花钱,通过work赚钱,都是对this.money直接操作
父通过v-model把money双向绑定到子,父子共享一个钱包
father.vue1 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
| <template> <div> <h1>我是爸爸</h1> <h3 v-for="(price,item) in store" @click="buy(item)">Buy {{ item }} (${{price}}) </h3> <h3 @click="work">Work work!</h3> <son v-model="money" title="儿子你好"></son> </div> </template>
<script> import Son from './son';
export default { name: 'Father', components: { Son }, data() { return { store: { car: 40, house: 50, }, personel: [], }, } methods: { buy(item) { if (this.money > this.store[item]) { this.money = this.money - this.store[item]; personal.push(item); } else { console.log('破产啦'); } }, work() { this.money += 1; }, }, } </script>
|
子通过value属性,接收了父的money
可以通过buy花钱,但不能直接操作money,而是通过this.$('input', newVal)
来更新父的money
son.vue1 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
| <template> <div> <h1> 我是儿子 </h1> <h3 v-for="(price,item) in store" @click="buy(item)">Buy {{ item }} (${{price}}) </h3> </div> </template>
<script> export default { name: 'Son', props: { value: { type: Number, default: ()=> 0, validator(val) { return val >= 0; }, } }, data() { return { store: { snack: 5, book: 10, game: 30, }, personel: [], }, } methods: { buy(item) { if (this.value > this.store[item]) { this.$('input', this.value - this.store[item]); personal.push(item); } else { console.log('破产啦'); } }, }, } </script>
|
总结:v-model本质上,是prop-$emit的语法糖
v-model="money"
相当于:value="money" @input="money=$event.target.value"
这也是,子组件必须使用value和input的原因
tips:上文的例子中,可以通过model选项,把默认的属性名value和事件名input,转为更业务化的money和spend
1 2 3 4 5 6 7 8 9 10 11 12
| model: { prop: 'money', event: 'spend', }, props: { money: ... }, methods: { buy(...) { this.$emit('spend',...); } },
|
父attribute.sync <-> 子$emit(‘update:attribute’, value)
.sync双向绑定方式,与v-model非常类似
父组件中,只是将v-model改为:money.sync
father.vue1 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
| <template> <div> <h1>我是爸爸</h1> <h3 v-for="(price,item) in store" @click="buy(item)">Buy {{ item }} (${{price}}) </h3> <h3 @click="work">Work work!</h3> <son :money.sync="money" title="儿子你好"></son> </div> </template>
<script> import Son from './son';
export default { name: 'Father', components: { Son }, data() { return { store: { car: 40, house: 50, }, personel: [], }, } methods: { buy(item) { if (this.money > this.store[item]) { this.money = this.money - this.store[item]; personal.push(item); } else { console.log('破产啦'); } }, work() { this.money += 1; }, }, } </script>
|
子组件将model选项去掉,因为已经不是v-model了
prop部分不变,仍是接收money属性
更新money部分,不再是this.$emit('input',newVal)
,而是this.$emit('update:money',newVal)
son.vue1 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
| <template> <div> <h1> 我是儿子 </h1> <h3 v-for="(price,item) in store" @click="buy(item)">Buy {{ item }} (${{price}}) </h3> </div> </template>
<script> export default { name: 'Son', props: { money: { type: Number, default: ()=> 0, validator(val) { return val >= 0; }, } }, data() { return { store: { snack: 5, book: 10, game: 30, }, personel: [], }, } methods: { buy(item) { if (this.money > this.store[item]) { this.$('update:money', this.money - this.store[item]); personal.push(item); } else { console.log('破产啦'); } }, }, } </script>
|
总结:.sync与v-model一样,也是一种语法糖,不同之处在于,.sync可以绑定多个属性:
1
| <son :money.sync="money" :house.sync="house" :car.sync="car"></son>
|
子<slot :attr="value">
-> 父<template #sonSlot="attr">
子组件通过slot将score传递给父组件
son.vue1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <div> <h3> 我是儿子 </h3> <slot :score="score" /> </div> </template>
<script> export default { name: 'Son', data() { return { score: { math: 100, chinese: 50, english: 66, }, }, } } </script>
|
父组件通过v-slot接收score,并在子组件内部以插槽形式渲染
father.vue1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <div> <h1>我是爸爸</h1> <son> <template v-slot="score"> 数学 : {{ score.math }} 语文 : {{ score.chinese }} 英语 : {{ score.english }} </template> </son> </div> </template>
<script> import Son from './son';
export default { name: 'Father', components: { Son }, } </script>
|
其他奇技淫巧
父子组件通信,其实还有:
这两种方法,都破坏了Vue的单向数据流
,是官方不推荐的,只在某些极限情况下会用到
兄弟组件通信、爷孙组件通信
跨层级或平级组件之间的通信,是很少见的
一般都会通过中间组件,进行数据的中转,而非直接通信
直接通信可以采用$bus总线、Vuex等,本文不再赘述
总结
方法 |
信息流向 |
绑定方式 |
prop |
父 -> 子 |
单向 |
$emit |
子 -> 父 |
单向 |
v-model |
父 <-> 子 |
双向 单值 |
.sync |
父 <-> 子 |
双向 多值 |
slot |
子 -> 父 |
单向 |
$refs |
父 -> 子 |
单向 |
$parent |
子 -> 父 |
单向 |
单向绑定: 适用于纯展示、非交互的组件,例如:统计图、列表…
双向绑定: 适用于数据交互的组件,例如:文本框、下拉框…
具体用哪一种,我也说不太清楚,凭经验吧(((逃