props
$emit
expose
/ ref
attrs
v-model
provide
/ inject
props
用 props
传数据给子组件有两种方法。
方法一:混合写法
父组件
<template><child :title="title" :desc="desc"></child></template><script>import child from './child.vue';import { ref, reactive } from 'vue';export default {data() {return {title: '标题',};},setup() {// 创建一个响应式数据// 写法一:适用于基础类型 ref 还有其他用处,下面章节有介绍const desc = ref('传递子组件的参数');// 写法二:适用于引用类型(其实基础类型也行),如数组、对象const desc = reactive(['传递子组件的参数']);return {desc,};},};</script>
子组件
<script>export default {// 如果这行不写,下面就接收不到props: ['title', 'desc'],setup(props) {console.log(props);// { title: "传递子组件的参数", desc: "传递子组件的参数" }},};</script>
方法二:纯 Vue3 写法
父组件
<template><child :title="title"></child></template><script setup>import child from './child.vue';import { ref, reactive } from 'vue';const title = ref('传递子组件的参数');// 或者复杂类型const title = reactive(['传递子组件的参数']);</script>
子组件
<script setup>import { defineProps } from "vue"const props = defineProps({// 写法一title: String// 写法二title:{type:String,default: ''}})console.log(props)// { title: "传递子组件的参数" }</script>
data
的属性,只能接收到父组件里 setup
函数里传的属性props
接收到 data
和 setup
函数里的属性,但是子组件要是在 setup
里接收,同样只能接收到父组件中 setup
函数里的属性,接收不到 data
里的属性官方也说了,既然用了 Vue3.x,就不要写 Vue2.x 了,所以不推荐混合写法。下面的例子,一律只用纯 Vue3 的写法,就不写混合写法了。
$emit
子组件派发事件
<template><!-- 写法一 --><button @click="emit('onChildClick')">按钮</buttom><!-- 写法二 --><button @click="handleButtonClick">按钮</buttom></template><script setup>// 方法一:适用于 Vue3.2 版本import { defineEmits } from "vue"// 对应写法一const emit = defineEmits(["onChildClick"])// 对应写法二const handleButtonClick = ()=>{emit("onChildClick", "这是发送给父组件的信息")}// 方法二:不适用于 Vue3.2版本,该版本 useContext() 已废弃import { useContext } from "vue"const { emit } = useContext()const handleButtonClick = ()=>{emit("onChildClick", "这是发送给父组件的信息")}</script>
父组件响应事件
<template><child @onChildClick="handleChildClick"></child></template><script setup>import child from './child.vue';const handleChildClick = (payload) => {console.log(payload);// 这是父组件收到的信息};</script>
expose
和 ref
父组件获取子组件的属性或者调用子组件方法
子组件
<script setup>// 方法一 适用于 Vue3.2 版本import { defineExpose } from 'vue';defineExpose({title: '子组件标题',handleTitleConsole() {console.log('这是子组件的方法');},});// 方法二 不适用于 Vue3.2 版本,该版本 useContext() 已废弃import { useContext } from 'vue';const ctx = useContext();// 对外暴露属性方法等都可以ctx.expose({title: '这是子组件的属性',handleTitleConsole() {console.log('这是子组件的方法');},});</script>
父组件
<template><child ref="childRef"></child><button @click="handleButtonClick">按钮</button></template><script setup>import child from './child.vue';import { ref } from 'vue';const childRef = ref(null);const handleButtonClick = () => {console.log(childRef.value.title);// 获取子组件对外暴露的属性childRef.value.handleTitleConsole();// 调用子组件对外暴露的方法};</script>
attrs
attrs
包含父作用域除 class
和 style
除外的非 props
属性集合
父组件
<template><child :title="title" :desc="desc" content="内容"></child></template><script setup>import child from './child.vue';import { ref } from 'vue';const title = ref('标题');const desc = ref('描述');</script>
子组件
<script setup>import { defineProps, useContext, useAttrs } from 'vue';const props = defineProps({title: String,});// 方法一 适用于 Vue3.2 版本const attrs = useAttrs();console.log(attrs);// { desc: "描述", content: "内容" }// 方法二 不适用于 Vue3.2 版本,该版本 useContext() 已废弃const ctx = useContext();// 如果没有用 props 接收 title 的话就是 { title: "标题", desc: "描述", content: "内容" }console.log(ctx.attrs);// { desc: "描述", content: "内容" }</script>
v-model
可以支持多个数据双向绑定
父组件
<child v-model:key="key" v-model:value="value"></child><script setup>import child from './child.vue';import { ref, reactive } from 'vue';const key = ref('key');const value = ref('value');</script>
子组件
<template><button @click="handlerClick">按钮</button></template><script setup>import { defineEmits, useContext } from 'vue';// 方法一 适用于 Vue3.2版本const emit = defineEmits(['key', 'value']);// 方法二 不适用于 Vue3.2 版本,该版本 useContext() 已废弃const { emit } = useContext();// 用法const handlerClick = () => {emit('update:key', 'new key');emit('update:value', 'new value');};</script>
provide
和 inject
provide / inject 为依赖注入
provide
:可以让我们指定想要提供给后代组件的数据或inject
:在任何后代组件中接收想要添加在这个组件上的数据,不管组件嵌套多深都可以直接拿来用父组件
<script setup>import { provide } from 'vue';provide('title', '标题');</script>
子组件
<script setup>import { inject } from 'vue';const title = inject('title');console.log(title);// '标题'</script>
父子通信的实现方式包括:
props
和 emit
v-model
$children
和 $parent
$listeners
和 $attrs
.sync
props
props
以单向数据流的形式可以很好地实现父子组件间的通信。
所谓单向数据流:就是数据只能通过 props
由父组件流向子组件,而子组件并不能通过修改 props
传过来的数据修改父组件的相应状态。
所有的
prop
都使得其父子prop
之间形成了一个单向下行绑定:父级prop
的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
props
传递参数给子组件emit
发射事件传递给父组件// 父组件Vue.component('parent', {template: `<div><p>This is parent component!</p><child :msg="msg" @getChildData="getChildData"></child></div>`,data() {return {msg: 'hello',};},methods: {// 执行子组件触发的事件getChildData(val) {console.log(val);},},});// 子组件Vue.component('child', {template: `<div><input type="text" v-model="myMessage" @input="passData(myMessage)"></div>`,props: ['message'],data() {return {// 这里是必要的,因为你不能直接修改 props 的值myMessage: this.message,};},methods: {passData(val) {// 数据状态变化时触发父组件中的事件this.$emit('getChildData', val);},},});var app = new Vue({el: '#app',template: `<div><parent /></div>`,});
this.$emit
触发父组件注册的 getChildData 事件处理数据逻辑$emit
🗑 在 Vue3 中,
$on
、$off
和$once
实例方法已被移除,组件实例不再实现事件触发接口。
v-model
v-model
是 props
和 emit
的语法糖,v-model
默认会解析成名为 value
的 props
和名为 input
的事件。
<!-- Parent Component --><template><children v-model="msg"></children><p>{{msg}}</p></template><script>export default {data() {return {msg: 'model',};},};</script>
<!-- Parent Component --><template><input :value="value" @input="onInput" /></template><script>export default {props: ['value'],methods: {onInput(e) {this.$emit('input', e.target.value);},},};</script>
$children
和 $parent
在父组件使用 $children
访问子组件。
在子组件中使用 $parent
访问父组件。
<!-- Parent Component --><child /><script>export default {data() {return {msg: 'data',};},methods: {onConsole() {console.log('Parent Method');},},mounted() {console.log(this.$children[0].foo);},};</script>
<!-- Child Component --><div>{{$parent.msg}}</div><script>export default {data() {return {foo: 'Child Data',};},mounted() {// 子组件执行父组件方法this.$parent.onConsole();},};</script>
$listeners
和 $attrs
Vue 2.4+
props
和 emit
这种通信方式只适合直接的父子组件,如果跨越层级的祖先/子孙组件,使用这样的方式在传递数据量大时会十分不便。
// 组件A GrandParentVue.component('grand-parent', {template: `<div><p>This is grandfather component!</p><B :c="c" :msg="b" @getC="getChildData" @getB="getParentData(b)"></B></div>`,data() {return {b: 'b',c: 'c', //传递给c组件的数据};},methods: {// 执行 B 子组件触发的事件getParentData(val) {console.log(`这是来自B组件的数据:${val}`);},// 执行 C 子组件触发的事件getChildData(val) {console.log(`这是来自C组件的数据:${val}`);},},});// 组件B ParentVue.component('parent', {template: `<div><input type="text" v-model="b" @input="passData(b)"><!-- C Child 组件中能直接触发 getC 的原因在于:B 组件调用 C 组件时,使用 v-on 绑定了 $listeners 属性 --><!-- 通过 v-bind 绑定 $attrs 属性,C 组件可以直接获取到 A 组件中传递下来的 props(除了 B 组件中 props 声明的) --><C v-bind="$attrs" v-on="$listeners"></C></div>`,/*** 得到父组件传递过来的数据* 这里的定义最好是写成数据校验的形式,免得得到的数据是我们意料之外的** props: {* a: {* type: String,* default: ''* }* }**/props: ['msg'],data() {return {b: this.msg,};},methods: {passData(val) {// 触发父组件中的事件this.$emit('getB', val);},},});// 组件C ChildVue.component('child', {template: `<div><input type="text" v-model="$attrs.c" @input="passCData($attrs.c)"></div>`,methods: {passCData(val) {// 触发父组件 A 中的事件this.$emit('getC', val);},},});var app = new Vue({el: '#app',template: `<div><grand-parent /></div>`,});
上述例子中,定义了 A、B、C 三个组件,其中组件 B 时组件 A 的子组件,组件 C 时组件 B 的子组件。
$attrs
和绑定了事件 $listeners
$attrs
属性,通过 v-on
绑定了 $listeners
$attrs
和 $listeners
$attrs
:包含父作用域中不被 prop 所识别(且获取)的特性绑定(class
和 style
除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定属性(class
和 style
除外),并且可以通过 v-bind="$attrs"
传入内部组件$listeners
:包含了父作用域中的(不含 .native
修饰符的)v-on
事件监听器。它可以通过 v-on="$listeners"
传入内部组件.sync
<!-- Parent Component --><child :count.sync="num" /><script>export default {data() {return {num: 0,};},};</script>
<div @click="handleAdd">Add</div><script>export default {data() {return {counter: this.count,};},props: ['count'],methods: {handleAdd() {this.$emit('update:count', ++this.counter);},},};</script>
可以通过查找父组件中的子组件实现,this.$parent.$children
在 $children
中可以通过组件 name 查询到需要的组件实例,然后进行通信。
Vue 中的 provide / inject 与 React 中的 Context API 相似。
在父组件中通过 provide
来提供属性,然后在子组件中通过 inject
来注入变量。不论子组件有多深,只要调用了 inject
那么就可以注入在 provide
中提供的数据,而不是局限于只能从当前父组件的 prop 属性来获取数据,只要在父组件的生命周期内,子组件都可以调用。
可以使用 provide / inject,虽然文档中不推荐直接使用在业务中。
假设有父组件 A,然后一个跨多层次的子组件 B。
// 定义 parent 组件Vue.component('parent', {template: `<div><p>This is parent component!</p><child></child></div>`,provide: {foo: 'foo',},data() {return {message: 'hello',};},});// 定义 child 组件Vue.component('child', {template: `<div><input type="tet" v-model="mymessage"></div>`,inject: ['foo'], // 得到父组件传递过来的数据data() {return {mymessage: this.for,};},});const app = new Vue({el: '#app',template: `<div><parent /></div>`,});
对于任意组件间的数据通信,可以采用 Vuex 和 Event Bus 进行数据传输。
事件总线又称为 EventBus。在 Vue 中可以使用 EventBus 来作为沟通桥梁的概念,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件,但也就是太方便所以若使用不慎,就会造成难以维护的灾难,因此才需要更完善的 Vuex 作为状态管理中心,将通知的概念上升到共享状态层次。
首先,先创建事件总线并将其导出,以便其他模块可以使用或者监听它。
// event-bus.jsimport Vue from 'vue';export const EventBus = new Vue();
你需要做的只是引入 Vue 并导出它的一个实例(在这种情况下,称它为 EventBus)。实质上它是一个与 DOM 和程序的其他部分完全解耦的组件,它具有的仅仅只是它的实例方法而已。
另一种方式,可以直接在项目中 main.js
初始化 EventBus:
这种方式初始化的 EventBus 是全局的事件总线。
// main.jsVue.prototype.$EventBus = new Vue();
EventBus 通过新建 Vue 事件 bus 对象,然后通过 bus.$emit
触发事件,bus.$on
监听触发的事件。
// 组件 AVue.component('A', {template: `<div><p>This is A component!</p><input type="text" v-model="msg" @input="onPassData(msg)"></div>`,data() {return {msg: 'Hello brother1',};},methods: {onPassData(val) {// 触发全局事件 globalEventthis.$EventBus.$emit('globalEvent', val);},},});// 组件 BVue.component('B', {template: `<div><p>this is B component!</p><p>组件 A 传递过来的数据:{{aMsg}}</p></div>`,data() {return {bMsg: 'hello brother2',aMsg: '',};},mounted() {//绑定全局事件globalEventthis.$EventBus.$on('globalEvent', (val) => {this.aMsg = val;});},});//定义中央事件总线const EventBus = new Vue();// 将中央事件总线赋值到 Vue.prototype 上,这样所有组件都能访问到了Vue.prototype.$EventBus = EventBus;const app = new Vue({el: '#app',template: `<div><A /><B /></div>`,});
上述实例中,我们定义了组件 A 和组件 B,但是组件 A 和组件 B 之间没有任何关系。
中央事件总线 EventBus 非常简单,就是任意组件和组件之间打交道,没有多余的业务逻辑,只需要在状态变化组件触发一个事件,然后在处理逻辑组件监听该事件就可以。
参考资料: