因为vue 的数据是单向流动的,这是为了避免数据污染。在官方文档中也说到:所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
大致归纳一下:父传子--传值、子传父--传事件
父传子:父组件可以传递任何类型的数据给子组件
如果传递的数据是对象格式的,那么在子组件 内部监听 watch 的时候,需要使用深度监听,也就是添加 deep : true ,也就是下面的子组件的监听方式,如果是别的格式的,例如,字符串、数字、布尔值、 数组格式等,那就是普通监听就好了
父组件代码:在父组件中 通过 v-bind 的缩写形式 :listData='listData' 绑定了data 内部的数据,第一个 listData 只是一个名字,为了方便辨认,所以写的相同。 第二个 listData 则是 data 内部的数据
<template>
<div>
<h3>我是father</h3>
<Children :listData='listData' :xxx='xxx' :listObj='listObj'></Children>
//子组件传递了一个数组、一个字符串、一个对象
</div>
</template>
<script>
import Children from './children' //引入子组件
export default {
data () {
return {
xxx:'123',
listData: [{
id: 1,
name: "TCL彩电",
price: 1000,
num: 1,
img: "../../../assets/a.jpg"
}],
listObj: {
name: "aaa",
age: 18
}
}
},
components : {
Children //注册子组件
},
}
</script>
子组件代码:
1、通过 props 接收父组件传递过来的数据,规范要求写出数据的类型以及默认值,如果数据是数组或对象形式的,需要使用函数返回,不然控制台会报错。
2、props 接收数据之后,需要使用数据,这个时候需要用到 watch 监听器。对象监听需要用到 deep 深度监听,如果需要组件第一次进来之后就开始监听数据,那么需要 添加 immediate: true
<template>
<div>
<h3>我是children</h3>
</div>
</template>
<script>
export default {
props: {
listData: {
type: Array,
default: () => []
},
xxx:{
type:String,
default : ''
},
listObj:{
type: Object,
default: () => {}
}
},
watch: {
listData:{
handler(n,o) {
console.log(n,o)
}
},
xxx:{
handler(n) {
console.log(n)
}
},
listObj:{
handler(n,o) {
console.log(n,o)
},
deep: true,
immediate: true,
},
}
};
</script>
子传父--传事件:子组件传递数据给父组件时存在三种方式,但是都是通过事件传递
1、父组件传递 函数类型的props 给子组件,实现子组件向父组件传递数据
在父组件中引入子组件,向子组件中 通过 v-bind( 简写为 : ) 绑定一个 test 属性 ,该 test 属性对应的值则是 methods 中定义的方法。
<template>
<div id="app">
<School :test="test"/>
</div>
</template>
methods: {
test(val) {
console.log(val,'这是子组件传递过来的数据')
},
},
定义子组件,以及子组件事件
<template>
<div>
<p class="demo" @click="goto">School组件</p>
</div>
</template>
在子组件中接收该 test 属性,定义数据,定义组件方法。其实props 可以直接写成一个数组,不去定义类型,默认值以及是否必传,但是推荐还是写全一点,这样编译的时候会校验,提高代码质量
export default ({
// props:['test'],
props: {
test: {
type: Function,
default: () => {},
required: true,
}
},
data() {
return {
msg:'子组件数据'
}
},
methods: {
goto() {
this.test(this.msg)
}
},
})
点击触发 goto 事件,找到当前 props 中接收的 test 函数 ( props 接收的参数,都被Vue 底层处理过之后放在了 当前组件实例对象上,所以可以直接通过 this.xxx 拿到 )
控制台上打印了子组件数据。可以看到子组件传递的数据被打印了,表示父组件中绑定的 test 事件被执行了
2、通过 v-on( @ ) 与 $emit 实现子组件向父组件传递数据
App 组件中引入 School 子组件,且绑定 自定义事件 test。
<School @test="test"/>
test(val) {
console.log(val,'这是子组件传递过来的数据')
},
子组件模板、数据、样式不变,只是 goto 方法内部逻辑变更
methods: {
goto() {
this.$emit('test',this.msg)
}
},
点击触发 goto 事件,通过 $emit 触发 test 事件,根据名称找到 父组件中的 test 属性对应的方法,执行该方法。结果与 props 传递函数参数一致
3、通过 ref 以及 $on、$emit 三个 api 实现 父组件通过 自定义事件接收子组件参数
$on :监听当前实例上的自定义事件。事件可以由 vm.$emit
触发。回调函数会接收所有传入事件触发函数的额外参数。
$emit :触发当前实例上的事件。附加参数都会传给监听器回调。
App 组件中引入 Schoo 组件,且给 School 子组件添加了 ref 属性,定义 test 函数
<School ref='student'/>
test(val) {
console.log(val,'这是子组件传递过来的数据')
},
如果想使用这个方法去获取子组件数据,就需要用到 $on() 这个方法。现在假设,当父组件挂载时,我就要获取到子组件的值,我就应该在 父组件 的 mounted 生命周期中 使用 this.$refs.xxx来获取当前组件的实例对象,至于 $on() 这个方法,则是 挂载到 Vue 实例对象的原型上的,所以 组件实例对象 和 Vue 实例对象 都能使用 $on() 。
在这里就是 通过 $on 注册或者叫创建了一个 qwe 的自定义事件,且该自定义事件的回调函数是写在 methods 中的 getname
mounted() {
this.$refs.student.$on('qwe', this.test)
}
父组件的工作已经完了,现在该看看子组件了。子组件更简单了,和上面 第二种方法一样,通过 $emit() 这个方法来触发父组件定义的 qwe 方法,且将子组件 数据传递出去。
goto() {
this.$emit('test',this.msg)
}
当我点击 School 组件时,执行 goto 方法,通过 $emit 触发父组件自定义的 qwe 方法,且将参数传递给父组件。
父组件通过 $on 监听 qwe 方法,发现被触发了,执行其回调函数 this.test,且 $emit 传递的参数,都会当做形参传递到回调函数中
$on 和 v-on 的区别
这么一看哈,其实我在子组件上使用 v-on( @ ) 和我使用 $on 做到的事情是一样的啊,那为啥还要来个 $on 这个玩意。
第一点:$on 只能监听当前实例上的自定义事件,而 v-on 用在普通元素上时,只能监听原生 DOM 事件。用在自定义元素组件上时,也可以监听子组件触发的自定义事件。
第二点:$on 更加灵活,如果我想要我的自定义事件异步绑定,通过 v-on 是无法做到的,因为 v-on 指令在 模板编译的时候,就被Vue 底层处理过了,在渲染的时候直接就会绑定事件,但是 $on 的自由度更高。