Vue 作为一个轻量级的前端框架,核心两大特性就是响应式编程和组件化。
本文针对组件之间传值做详细讲解。 Vue就是由一个一个的组件构成的,组件化是它的精髓,也是最强大的功能之一。而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。
但在实际项目开发过程中,我们需要访问其他组件的数据,这样就就有了组件通信的问题。在 vue 中组件之间的关系有:父子,兄弟,隔代。针对不同的关系,怎么实现数据传递,下面展开说明。
如上图所示:
父子关系:A与B,A与C,B与D,C与E
兄弟关系:B与C
隔代关系(可能隔更多代):A与D,A与E
跨级关系:B与E,D与E等
一、父组件向子组件传值 props
<!--父组件页面-->
<template><div id="app"><!--子组件--><child-box :message="params"></child-box> //把params的值传给子组件</div>
</template><script>
// 引入子组件
import ChildBox from './childBox.vue'export default {// 初始化子组件components: {ChildBox},data() {return {params: 'hello' // 如果params值为 undefined 则会使用default的值}}
}
</script>
<!--子组件页面-->
<template><div class="child-box">{{ message }}</div>
</template><script>export default {/*** props功能:让组件接受外部传过来的数据* * 1.传递数据 <child-box :message="hello"></child-box>* * 2.接收数据* 第一种方式(只接收)* props: ['message']* * 第二种方式(限制类型)* props: { message: String } 多个 props: { message: String, name: '王新焱'}* * 第三种方式(限制类型、限制必要性、指定默认值) //这种方式更为严谨* props: {* message: {* type: String,* required: true,* default: '王新焱'* }* }* * 备注:props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据* * 如果要使用默认值 default message值设置为 undefined即可 ->示例:<child-box :message="undefined"></child-box>* */props: {message: {type: String, // 指定数据类型required: true, // 指定数据为必填项default: '王新焱' // 指定默认值,如果message绑定的值设置为 undefined 即会使用该值}}}
</script>
总结:
1.父组件中注册子组件,在子组件标签中添加子组件props中创建的属性,把需要传给子组件的值赋给该属性。
2.子组件在props中创建一个属性,用以接收父组件传过来的值。
3.父子组件的关系可以总结为prop向下传递,事件向上传递。
4.父组件通过prop给子组件下发数据,子组件通过事件给父组件发送信息,这就是单向数据流的表现形式。
二、子组件向父组件传值 $emit
<!-- 父组件页面 -->
<template><div id="app"><!-- 引入子组件,定义一个v-on的方法监听子组件的状态--><child-box :message="params" @childFnGetParent="parentFn"></child-box><P>{{ msg }}</P></div>
</template><script>
// 引入子组件
import ChildBox from './childBox.vue'export default {// 初始化子组件components: {ChildBox},data() {return {params: 'hello',msg: ''}},methods: {parentFn(val) {// val 就是子组件传过来的值this.msg = val}}
}
</script>
<!-- 子组件页面 -->
<template><div class="child"><button @click="parentFn">子组件向父组件传值</button></div>
</template><script>export default {data() {return {msg: '我是来自子组件的消息'}},methods: {parentFn() {// childFnGetParent 是在父组件v-on监听的方法,第二个参数this.msg是需要传递的值this.$emit('childFnGetParent', this.msg)}}}
</script>
总结
1.子组件中需要以某种方式例如点击事件的方法来触发一个自定义事件
2.将需要传的值作为$emit的第二个参数,该值将作为实参传给响应自定义事件的方法
3.在父组件中注册子组件并在子组件标签上绑定对自定义事件的监听
4.在通信中,无论是子组件向父组件传值还是父组件向子组件传值,他们都有一个共同点就是有中间介质,子向父的介质是自定义事件,父向子的介质是props中的属性。理解这两点对于父子通信就好理解了
子组件:通过$emit()方法发布事件广播
父组件:捕获到子组件向外触发的事件,然后可执行相应的方法
三、非父子组件传值 EventBus
非父子组件最常用的是EventBus方案进行数据传递,定义方式有三种
/*** 方法一 抽离成一个单独的 js 文件 EventBus.js ,然后在需要的地方引入* EventBus.js*/
import Vue from "vue"
export default new Vue()/*** 方法二 直接挂载到全局* main.js*/
import Vue from "vue"
Vue.prototype.$bus = new Vue()/*** 方法三 注入到 Vue 根对象上* main.js*/
import Vue from "vue"
new Vue({el:"#app",data:{Bus: new Vue()}
})
本文案例以第一种方法 新建 EventBus 展开讲述
1.新建公共文件 EventBus.js
/*** EventBus.js* * eventBus 又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心* * 不管是父子组件,兄弟组件,跨层级组件等都可以使用它完成通信操作*/import Vue from 'vue'// 创建一个 EventBus.js 文件,暴露一个 vue 实例
export default new Vue()
2.新建一个A组件
<!--组件A-->
<template><div class="child-A"><span>年龄:{{ value }}</span><button @click="getValue">修改年龄</button></div>
</template><script>
import Bus from './EventBus.js'
export default {data() {return {value: 30}},methods: {getValue() {// 在A组件中通过$emit向外部发送自定义事件 (事件广播)Bus.$emit('getVal', this.value)}}
}
</script>
3.新建一个B组件
<!--组件B-->
<template><div class="child-B"><span>年龄:{{ age }}</span><button @click="getData">点击触发</button></div>
</template><script>
import Bus from './EventBus.js'
export default {data() {return {age: 18}},mounted() {// B组件 用$on事件来接收外部事件Bus.$on('getVal', (data)=> {this.age = data // data为广播过来值})},methods: {getData() {this.age++}}
}
</script>
使用 EventBus 有一个弊端就是事件广播,这种方式不会自动销毁,所以避免回调函数重复执行,需要在destroyed生命周期中销毁广播事件
destroyed() {
Bus.$off('eventName') // 对Bus取消事件监听后 内存得到了释放
}
总结
$off() 会取消所有的事件订阅
$off('事件名') 会取消指定事件名的
$off('事件名', 回调) 会取消指定事件名的,指定回调
四、多层父子组件通信 (依赖注入) provide / inject
有时需要实现通信的两个组件不是直接的父子组件,而是祖父和孙子,或者是跨越了更多层级的父子组件,这种时候就不可能由子组件一级一级的向上传递参数,特别是在组件层级比较深,嵌套比较多的情况下,需要传递的事件和属性较多,会导致代码很混乱。
这时就需要用到 vue 提供的更高阶的方法:provide/inject
provide/inject:简单来说就是在父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量,不管组件层级有多深,只要调用了inject 那么就可以注入provide中的数据。
<!--父组件页面-->
<template><div class="box"><!-- 引入子组件A--><child-a></child-a><!-- 引入子组件B--><child-b></child-b></div>
</template><script>
export default {provide: {name: '张三丰', // 将变量name提供给它的所有子组件及后代组件reload: this.reload // 也可以是一个函数},data() {return {isShow: false}},methods: {reload() {this.isShow = true}}
}
</script>
<!--组件A-->
<template><div class="child-A"><div>这是组件A</div><!-- 输出张三丰 --><div>{{ name }}</div></div>
</template><script>export default {inject: ['name'], // 注入了从父组件中提供的name变量mounted() {console.log(this.name) // 张三丰}
}
</script>
<!--组件B-->
<template><div class="child-B"><div>这是组件B</div><!-- 输出张三丰 --><div>{{ name }}</div></div>
</template><script>
export default {inject: ['name'], // 注入了从父组件中提供的name变量mounted() {console.log(this.name) // 张三丰}
}
</script>
注:provide 和 inject 绑定并不是可响应的。即父组件的name变化后,子组件不会跟着变。
五、通过访问组件实例的方式 ref
父组件通过 ref获取子组件的实例 可以直接访问子组件里面的方法和属性
<!--父组件页面-->
<template><div class="box"><!-- 引入子组件A--><child-a ref="childA"></child-a><button @click="getChildData">获取子组件的属性和方法</button></div>
</template><script>
// 引入子组件
import ChildA from './childBox.vue'export default {// 初始化子组件components: {ChildA},methods: {getChildData() {// 获取子组件的属性console.log(this.$refs.childA.txt) //我是子组件A// 获取子组件的方法console.log(this.$refs.childA.getName()) //张三丰}}
}
</script>
<!--组件A-->
<template><div class="child-A"><div>这是组件A</div></div>
</template><script>export default {data() {return {txt: '我是子组件A',name: '张三丰'}},methods: {getName () {console.log(this.name)} }
}
</script>
注:这种方式的组件通信不能跨级
六、$children / $parent
$children:获取到一个组件实例,包含所有子组件(不包含孙子组件)的 VueComponent 对象数组,可以直接拿到子组件中所有数据和方法等
$parent:获取到一个组件实例,包含父节点的 VueComponent 对象,同样包含父节点中所有数据和方法
$children 方法讲解
<!--父组件页面-->
<template><div class="box"><!-- 引入子组件A--><child-a ref="childA"></child-a><!-- 引入子组件B--><child-b></child-b><button @click="getChildData">获取子组件的属性和方法</button></div>
</template><script>
// 引入子组件
import ChildA from './childBox.vue'
import ChildB from './childBox2.vue'export default {// 初始化子组件components: {ChildA,ChildB},methods: {getChildData() {// 获取第一个组件的nameconsole.log(this.$children[0].name) //张三丰// 获取第一个组件getTitle方法console.log(this.$children[0].getTitle()) //我是子组件A// 获取第二个组件的nameconsole.log(this.$children[1].name) //张无忌}}
}
</script>
<!--组件A-->
<template><div class="child-A"><div>这是组件A</div></div>
</template><script>export default {data() {return {txt: '我是子组件A',name: '张三丰'}},methods: {getTitle () {console.log(this.txt)} }
}
</script>
<template><div class="child-B"><div>这是组件B</div></div>
</template><script>
export default {data() {return {txt: '我是子组件B',name: '张无忌'}}
}
</script>
$parent 方法讲解
<!--父组件页面-->
<template><div class="box"><!-- 引入子组件A--><child-a ref="childA"></child-a></div>
</template><script>
// 引入子组件
import ChildA from './childBox.vue'export default {// 初始化子组件components: {ChildA},data() {return {title: '这是父页面',name: '父组件',age: 20}},methods: {getAge() {console.log(this.age)}}
}
</script>
<!--组件A-->
<template><div class="child-A"><div>这是组件A</div><button @click="getParentData">A组件获取父组件的属性和方法</button></div>
</template><script>export default {methods: {getParentData() {console.log(this.$parent.title) //获取父页面title属性console.log(this.$parent.getAge()) //执行父页面getAge方法}}
}
</script>
七、slot 插槽传值
子组件的数据通过插槽的方式传给父组件使用,要显示内容由父组件决定
<!--父组件页面-->
<template><div class="box"><!--引入子组件A--><child-a ref="childA" v-slot="slotProps"><!--父组件使用slot插槽方式获取子组件数据--> {{ slotProps.user.name }}{{ name }}</child-a></div>
</template><script>
// 引入子组件
import ChildA from './childBox.vue'export default {// 初始化子组件components: {ChildA},data() {return {name: '张三丰'}}
}
</script>
<!--组件A-->
<template><div class="child-A"><!-- <div>这是组件A</div> --><slot :user="user"></slot></div>
</template><script>export default {data() {return {user: {name: '张无忌'}}}
}
</script>
以上几种也是vue组件传值主流的方式了,根据业务的不同场景选择不同的方式,此外vuex也是组件传值最常用的方式,我计划把这个知识点单独列出来进行讲解,后续会附上链接。
友情赠送一张小图