# 父子组件之间🧑👶
# 1. props[父->子传值] & $emit[子->父发射事件]
父组件通过 :propName='xxxx'
的方式传递值,子组件用 props:{propName:{...}}
进行接收
子组件通过 this.$emit('sonEventName'[,param1[,param2]])
的形式传出事件名 sonEventName
和参数 param1,param2...
,父组件在自己的子组件标签上用 v-on
绑定: @sonEventName = 'parEventName[(param1[,param2])]'
的方式接收执行。
父组件
<template>
<div>
父组件:{{money}}
<son1 :money='money' @addMoney = 'addMoney'></son1>
</div>
</template>
<script>
import son1 from './son1'
export default {
components:{
son1
},
data(){
return {
money:100
}
},
methods:{
addMoney(val){
this.money = val
}
}
}
</script>
子组件
<template>
<div>
儿子1获得{{money}}
<button @click="raiseMoney">多给点嘛</button>
</div>
</template>
<script>
export default {
props:{
money:{
type:Number,
default:1
}
},
methods:{
raiseMoney(){
this.$emit('addMoney',200)
}
}
}
</script>
# 2. $parent 和 $children/$refs 获取父子元素
- $parent
在子组件中使用 this.$parent
可以获取到父实例,也可以使用 this.$parent.xxdata = 'xxx'
的方式修改父元素的值
- $root
另外提一下, $root
是会指向当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
- $children
父元素获取子元素的方式,由于 $children
返回的是一个数组 ,所以根据官方文档所说:
需要注意
$children
并不保证顺序,也不是响应式的。
使用时需要谨慎
$parent 和 $children 是不利于组件复用的,不是非常推荐。
**
- $refs
$refs
可以给每一个父元素中使用的子组件起别名,就可以精准使用自己需要的子组件
用法:
// 父组件
<son ref="aSon"/>
mounted(){
console.log(this.$refs.aSon) //即可拿到子组件的实例,就可以直接操作 data 和 methods
}
# 3. .sync 和 v-model
.sync
- 需求需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。
// 父组件
<son1 :money.sync='money'></son1>
//编译时会被扩展为
<son1 :money="money" @update:money="(val)=>this.money=val"></son1>
// 子组件
// 所以子组件可以通过$emit 触发 update 方法改变
this.$emit('update:money',200)
自定义组件的 v-model
根据官网的介绍:一个组件上的
v-model
默认会利用名为value
的 prop 和名为input
的事件,即:
<son1 :value="money" @input="(val)=>this.money=val></son1>
也可以写成
<son1 v-model="money"></son1>
但是 v-model
这种方法,子组件只能接收 value
这一个默认的 prop 具有局限性,而 .sync
可以随意起名字,官方也提供了一个扩展的办法,详见这里
# 祖孙组件之间👴👶
# 1. $attrs 和 $listeners
- $attrs
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (
class
和style
除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class
和style
除外),并且可以通过v-bind="$attrs"
传入内部组件——在创建高级别的组件时非常有用。
先来看一段代码:
<template>
<div>
<p>我是父组件</p>
<test name="tom" age="12" id="12345" class="child" style="color: red" />
</div>
</template>
<script>
export default {
components: {
test: {
template: `
<div>
<p>我是子组件</p>
<test2 v-bind="$attrs" s1="sss" s2="sss" />
</div>`,
props: ["name"],
created() {
console.log(this.$attrs); // {age: 12, id: 12345}
},
components: {
test2: {
template: `<p>我是孙子组件</p>`,
props: ["age", "s1"],
created() {
console.log(this.$attrs); // {s2: "sss", id: 12345}
}
}
}
}
}
};
</script>
我们一层一层来分析:
- 父级发送给下级 name age id class style 五个参数,由于 class 和 style 不属于范畴
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (
class
和style
除外)
所以只传下去 name age id
- 子组件的
props
内已经声明了 name ,所以this.$attrs
只剩下{age: 12, id: 12345}
- 子组件又给孙组件传了 s1 和 s2 ,(可以假定认为此时
this.$attr
)但是孙组件的props
又声明了 age 和 s1,那么this.$attrs
只剩下{s2: "sss", id: 12345}
,而 age 和 id 是父组件传过来的,此时就已经传到孙组件了
也就是说所接受到的属性是不包含class、style、和已经被prop获取的到的,接下来看一下子组件中的配置项inheritAttrs:
默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置
inheritAttrs
到false
,这些默认行为将会被去掉。而通过 (同样是 2.4 新增的) 实例 property$attrs
可以让这些 attribute 生效,且可以通过v-bind
显性的绑定到非根元素上。
翻译成人话就是把 $attr
上的没有在 props
里的值扔到当前组件的根元素上,具体是啥样呢:
<template>
<div>
<p>我是父组件</p>
<test name="tom" age="12" class="child" style="color: red" />
</div>
</template>
<script>
export default {
components: {
test: {
template: `<p>我是子组件</p>`,
props: ["name"],
inheritAttrs: true, // 默认为 true
created() {
console.log(this.$attrs); // {age: "12"}
}
}
}
};
</script>
浏览器解析出来就是这样的:age在根上
<div>
<p data-v-469af010>我是父组件</p>
<p data-v-469af010 age="12" class="child" style="color: red;">我是子组件</p>
</div>
如果设置 inheritAttrs: false
或者不设置, age就没了:
<div>
<p data-v-469af010>我是父组件</p>
<p data-v-469af010 class="child" style="color: red;">我是子组件</p>
</div>
- $listeners
包含了父作用域中的 (不含
.native
修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件——在创建更高层次的组件时非常有用。
让我们看看下面的代码做了什么:
- 类似于我们使用
$emit
,我们这里在孙组件使用了this.$listeners.methodName([param1[,param2]])
的形式,向外发射了一个事件 todo - 在子组件
sonButton
里用v-on="$listeners"
传递这个事件,这个写法类似于v-bind="$attrs"
- 在父组件中,用
@methodName="[父组件 method 名]"
来使用,这个和正常父子通信一样的 - 注意孙组件和子组件的打印内容,都是会完整的拿到父组件绑定的所有方法
<div id="app">
{{msg}}
<son-button @todo="handleClick"></son-button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.7/vue.common.dev.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: ''
},
methods: {
handleClick(grandsonMsg) {
this.msg = grandsonMsg
}
},
components: {
'sonButton': {
template: `<grand-son-button v-on="$listeners"></grand-son-button>`,
created() {
console.log('son listeners',this.$listeners) // 包含父级所有绑定的方法
},
components: {
'grandSonButton': {
template: `<button @click="grandSonClick">grandSon</button>`,
created() {
console.log('grandSon listeners',this.$listeners) // 包含父级所有绑定的方法
},
methods: {
grandSonClick() {
this.$listeners.todo('a msg from grandson')
}
}
},
}
},
}
})
</script>
运行效果:
# 2. provide 和 inject
先看下官网的介绍:
- provide:
Object | () => Object
- inject:
Array<string> | { [key: string]: string | Symbol | Object }
- 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。
provide
选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。
inject
选项应该是:
- 一个字符串数组,或
- 一个对象,对象的 key 是本地的绑定名,value 是:
- 在可用的注入内容中搜索用的 key (字符串或 Symbol),或
- 一个对象,该对象的:
- `from` property 是在可用的注入内容中搜索用的 key (字符串或 Symbol) - `default` property 是降级情况下使用的 value
非响应式的数据传值代码演示
// parent 父组件
<template>
<div>
<h1 ref="h1">父组件</h1>
<p @click="changeData">{{ msg }}</p>
<children ref="children"></children>
</div>
</template>
<script>
import Children from './children';
export default {
name: 'praent',
components: {
Children
},
data() {
return {
msg: '来自父亲的值'
};
},
methods: {
changeData() {
this.msg = '父组件改变了向下传递的msg值';
}
},
provide() {
return {
msg: this.msg
};
}
};
</script>
// children 子组件
<template>
<div class="children">
<h2 ref="h2">子节点</h2>
<p>{{ msg }}</p>
<grandson></grandson>
</div>
</template>
<script>
import Grandson from './grandson';
export default {
name: 'children',
components: {
Grandson
},
inject: ['msg']
};
</script>
启动页面,点击父组件p标签,父组件的值已经修改了,向下传递的值并没有修改
如果传入的值为响应式的
<template>
<div>
<h1 ref="h1">父组件</h1>
<p @click="changeData">{{ msg.value }}</p>
<children ref="children"></children>
</div>
</template>
<script>
import Children from './children';
export default {
name: 'praent',
components: {
Children
},
data() {
return {
// 稍微把msg的值改为一个对象
msg: { value: '来自父亲的值' }
};
},
methods: {
changeData() {
this.msg.value = '父组件改变了向下传递的msg值';
}
},
provide() {
return {
msg: this.msg
};
}
};
</script>
// children 子组件
<template>
<div class="children">
<h2 ref="h2">子节点</h2>
<p>{{ msg.value }}</p>
<grandson></grandson>
</div>
</template>
<script>
import Grandson from './grandson';
export default {
name: 'children',
components: {
Grandson
},
inject: ['msg']
};
</script>
// grandson 孙组件
<template>
<div class="grandson">
<h3>孙组件</h3>
<p>{{ msg.value }}</p>
</div>
</template>
<script>
export default {
name: 'grandson'
inject: ['msg']
};
</script>
启动页面,此时再去点击父组件的p标签,现在不单单父组件的值更新了,连同子组件和孙组件的值都发生变化了
将孙组件稍微修改一下,定义一个值去存储inject接受到的响应式的值,再通过点击事件去修改值,
<template>
<div class="grandson">
<h3>孙组件</h3>
<p @click="changeMsg">{{ grandsonMsg.value }}</p>
</div>
</template>
<script>
export default {
name: 'grandson',
data() {
return {
// 用来保存inject接收到的整个【msg对象】
grandsonMsg: ''
}
},
mounted(){
this.grandsonMsg = this.msg
},
inject: ['msg'],
methods:{
changeMsg(){
// 改变inject接收到的值,可以改变整条链路上的msg对象的value
this.grandsonMsg.value = '孙组件修改了msg值'
}
}
};
</script>
可以看到,孙组件接受到这个响应式的值,然后通过这种的方式去修改这个值,同样可以修改到父组件provide下来的值
# 其他传递(兄弟节点或长距离)🧒👦
# 1. 事件总线$bus
1.1 场景
这里以项目中实际场景举例:
我想要在GoodsListItem
组件中每一个item
加载完后调用一次Scroll
组件发送给Home
的refresh
事件,但是 GoodsListItem
是GoogsItem
组件的子组件,不能够直接去Home
拿事件,由于组件离得太远,一层层传事件过于麻烦,这时候就用上了事件总线$bus
来管理这个事件
1.2 使用
**
- main.js中在Vue原型链上把他加上
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.prototype.$bus = new Vue() // event Bus 用于无关系组件间的通信。
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
- 下级组件中发射事件
使用this.$bus.$emit('事件名',params)
GoodsListItem.vue
img
标签中使用@load
监听图片加载完成时间
相当于原生js的<img src="xxxxx" onload="imgLoad()">
<img :src="goodsItem.show.img" alt="" @load="imgLoad"/>
methods:{
imgLoad(){
// 这里使用事件总线$bus $bus和vuex区别在于$bus是用于管理事件的监听 vuex主要管理状态
this.$bus.$emit('itemImgLoad')
}
}
- 上级组件拿到事件
使用this.$bus.$on('事件名',(params)=>{do sth...})
Home.vue
created() {
// 3.监听事件总线的图片加载事件
this.$bus.$on("itemImgLoad", () => {
// 作用是可以在图片加载后重新计算现在可滚动区域的高度,避免图片还没加载结束就已经算好高度,或者切换类别的时候用上一个类别的高度
this.$refs.scroll.scroll.refresh();
});
},
# 2. vuex
参考 vuex 专门的文章