Vue父子组件通信
在vue项目中,父子组件之间的通信很常见也很重要,父组件通过props将数据传递给子组件,子组件通过触发自定义事件将数据传递给父组件。
一、父子组件之间通信
我们先来看一个简单的例子(vue官网的例子):
背景:创建一个博文组件,组件包含标题、正文和放大博文字号按钮。我们希望能够通过向组件中传入标题和正文来创建一个博文组件,并能够点击按钮放大博文的字号,但要保证页面其他地方的字号不变。
可以直接使用props将标题、正文数据传入组件中,这样我们实现了父组件向子组件的数据传递。当我们点击子组件的放大按钮时,希望将字号放大数值传递给父组件,并由父组件执行放大字号的操作(当然在子组件中可以直接实现字号放大,但是这里是想展示子组件的数据传递给父组件)。
本文没有详细介绍props,如果你想详细了解prop类型、验证等知识,传送门:Prop
代码: 父组件中
-
父组件中,在使用子组件时,通过@enlarge-text="onEnlargeText"来监听子组件抛出的enlarge-text事件,一旦监听到该事件,即执行onEnlargeText方法改变字号
-
@是v-on:的缩写,@后的内容enlarge-text表示监听的子组件的自定义事件名,该名称必须和子组件中定义的事件名保持一致,推荐使用短横线分隔命名
-
父组件上的attribute(title、content)绑定的数据可以传到子组件,只要我们在子组件中使用props接受
<template>
<div class="event_custom">
<blog-post
@enlarge-text="onEnlargeText"
:title="title"
:content="content"
:style="{ fontSize: postFontSize + 'px' }"
></blog-post>
</div>
</template>
<script>
import BlogPost from "./EventCustomChild.vue";
export default {
components: {
BlogPost,
},
data() {
return {
title: "标题",
content:
"正文正文正文正文正文正文正文正文",
postFontSize: 16,
};
},
methods: {
onEnlargeText(enlargeAmount) {
this.postFontSize += enlargeAmount;
},
},
};
</script>
代码:子组件
-
在子组件的props中,接受父组件传递的数据title和content
-
在子组件的button按钮上绑定了点击事件,一旦点击按钮,会调用enlargeText方法,执行$emit方法
-
$emit方法第一个参数为自定义事件名,由于v-on事件监听器在dom模板中会自动转换为全小写,所以v-on:myEvent会变成v-on:myevent,如果为自定义事件命名为myEvent,则父组件将监听不到自定义事件,因此我们最好使用kebab-case命名
-
$emit方法第一个参数之后的参数,可以是一个/多个,可以是字符串、数组、对象、函数等。参数值将会传递给监听器回调,父组件监听到事件后会调用onEnlargeText方法,该方法的参数即为自定义事件抛出的参数。
<template>
<div class="blog-post">
<div class="title">{{ title }}</div>
<div class="content">{{ content }}</div>
<button @click="enlargeText">放大字号</button>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: "",
},
content: {
type: String,
default: "",
},
},
data() {
return {};
},
methods: {
// 点击按钮调用这个方法,通过$emit方法,并传入事件名和参数(可选),来触发事件enlarge-text
// 这个事件会被父组件监听到,前提是我们触发了事件,并在父组件中配置了事件监听
enlargeText() {
this.$emit("enlarge-text", 1);
},
},
};
</script>
二、自定义组件实现V-model
v-model本质是语法糖,下面两行代码是等价的,通过input事件监听输入框中数据的变化,并将输入框中的value值与变量name绑定,因此输入框中的数据发生变化,name会发生变化,页面中使用name的地方也会相应改变。
<input v-model="name">
<input v-bind:value="name" @input="name = $event.target.value">
在父子组件通信中,父组件通过v-bind绑定向子组件传递的数据,监听子组件的自定义事件,v-model既绑定了数据,又监听了事件,我们可以通过自定义组件实现v-model。
代码:父组件(子组件命名为vInput)
-
子组件的props通过value属性获取父组件的name数据(v-bind:value="name")
-
父组件监听子组件通过$emit方法抛出的input事件,并将值赋值给name
<template>
<div class="event_custom">
<vInput v-model="name"></vInput>
<div>{{ name }}</div>
</div>
</template>
<script>
import vInput from "./EventCustomChild.vue";
export default {
components: {
vInput,
},
data() {
return {
name: "",
};
},
};
</script>
代码:子组件
<template>
<div class="input">
<input type="text" :value="value" @input="onInput" />
</div>
</template>
<script>
export default {
props: ["value"],
methods: {
onInput(event) {
this.$emit("input", event.target.value);
},
},
};
</script>
一个组件上的
v-model
默认会利用名为value
的 prop 和名为input
的事件,但是像单选框、复选框等类型的输入控件可能会将value
attribute 用于不同的目的——vue官网。
v-model默认props名为value,事件为input事件。如果我们定义组件时想使用props的名为checked,事件为change,vue提供了一个model选项。在model选项中,我们定义了prop的值为checked,event的值为change,那么相当于
<input v-model="isChecked">
<input v-bind:checked="isChecked" @change="isChecked = $event.target.checked">
<template>
<div class="event_custom">
<checkbox v-model="isChecked"></checkbox>
<div>isChecked的值为:{{ isChecked }}</div>
</div>
</template>
<script>
import checkbox from "./EventCustomChild.vue";
export default {
components: {
checkbox,
},
data() {
return {
isChecked: false,
};
},
};
</script>
<template>
<div class="input">
<input type="checkbox" :checked="checked" @change="onChange" />
</div>
</template>
<script>
export default {
model: {
prop: "checked",
event: "change",
},
props: {
checked: Boolean,
},
methods: {
onChange(event) {
this.$emit("change", event.target.checked);
},
},
};
</script>
三、.sync
.sync也是一个语法糖
vue自动为父组件创建一个事件监听器,用来改变父组件的值,事件的名为update:prop名。假如prop名为title,那么事件名为update:title,事件的目的是为了更新title的值
<text-document :title.sync="text"></text-document>
//相当于
<text-document :title="text" @update:title="newTitle=>text=newTitle"></text-document>
this.$emit("update:title", newTitle);
代码:父组件
<template>
<div class="event_custom">
<textDecoration :title.sync="text"></textDecoration>
<div>text: {{ text }}</div>
</div>
</template>
<script>
import textDecoration from "./EventCustomChild.vue";
export default {
components: {
textDecoration,
},
data() {
return {
text: "1",
};
},
};
</script>
代码:子组件
<template>
<div class="input">
<div @click="handleTitle">在子组件中点击改变title值</div>
</div>
</template>
<script>
export default {
props: ["title"],
methods: {
handleTitle() {
this.$emit("update:title", "123");
},
},
};
</script>
注意带有
.sync
修饰符的v-bind
不能和表达式一起使用 (例如v-bind:title.sync=”doc.title + ‘!’”
是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似v-model
。
将
v-bind.sync
用在一个字面量的对象上,例如v-bind.sync=”{ title: doc.title }”
,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
四、$attrs和$listeners
$attrs:包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (
class
和style
除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class
和style
除外),并且可以通过v-bind="$attrs"
传入内部组件——在创建高级别的组件时非常有用。 ————vue官网
$listeners:包含了父作用域中的 (不含
.native
修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件——在创建更高层次的组件时非常有用。 —vue官网
inheritAttrs:默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。也就是说,父组件中没有使用props接受的attribute会显示在子组件的根元素上。
简单地说:通过子组件作为中间层,孙子组件可以拿到父组件的数据,触发父组件的监听器
我们通过代码理解一下:
代码:父组件
<template>
<div class="father">
<div>爸爸</div>
<son
:text="text" //父组件向子组件传入text、msg和content
:msg="msg"
:content="content"
@handle1="handle1" //父组件中定义事件监听器,监听handle1和handle2事件
@handle2="handle2"
></son>
</div>
</template>
<script>
import son from "./EventCustomChild.vue";
export default {
components: {
son,
},
data() {
return {
text: "1",
msg: "12",
content: "123",
};
},
methods: {
handle1() {
console.log("父组件的handle1函数");
},
handle2() {
console.log("父组件的handle2函数");
},
},
};
</script>
代码: 子组件
<template>
<div class="son">
<div @click="handle">子组件</div>
<grandson v-bind="$attrs" v-on="$listeners" @handle1="handle1"></grandson>
//子组件中通过 v-bind="$attrs" v-on="$listeners" 将父组件的数据和监听器传递给孙子组件
//子组件中定义事件监听器,监听handle1事件
</div>
</template>
<script>
import grandson from "./EventCustomChildChild.vue";
export default {
inheritAttrs: false,//为true,子组件的根元素为<div class="son" msg="12" content="123"> 没有被props接收的attribute会显示
//为false,子组件的根元素为<div class="son">
components: {
grandson,
},
props: ["text"],
methods: {
handle() {
console.log(this.$attrs);//{msg: '12', content: '123'},获取父组件传过来,没有被props接收
console.log(this.$listeners); //{handle1: ƒ, handle2: ƒ},获取父作用域中的(不含 `.native` 修饰器的) `v-on` 事件监听器
},
handle1() {
console.log("儿子组件中的handle1");
},
},
};
</script>
代码:孙子组件
<template>
<div class="grandson">
<div @click="handle">孙子组件</div>
</div>
</template>
<script>
export default {
data() {
return {};
},
props: ["msg"],
methods: {
handle() {
console.log(this.$attrs);//{content: '123'},子组件通过v-bind="$attrs"将非props传递到孙子组件
//如果没有使用props,结果为{msg: '12', content: '123'},但如果调用孙子组件时,发生下述情况,msg值会被覆盖,结果变为{msg: '11111', content: '123'}
//<grandson v-bind="$attrs" v-on="$listeners" @handle1="handle1" :msg="11111"></grandson>
console.log(this.$listeners);//{handle1: ƒ, handle2: ƒ},获取上级组件的所有(不含 `.native` 修饰器的) `v-on` 事件监听器
//子组件和父组件都定义了handle1事件监听器,但他们不会覆盖,展开handle1,会发现里面包含两个函数
this.$emit("handle1");//儿子组件中的handle1、父组件的handle1函数,先触发子组件,再触发父组件
this.$emit("handle2");//父组件的handle2函数
},
},
};
</script>
我们可以使用$attrs和$listeners封装第三方插件,我们希望二次封装的插件不仅支持自己定义的属性,也支持第三方插件原有的属性,使用$attrs和$listeners可以直接把第三方插件的属性和监听器传递给用户,否则我们可能要使用props接收第三方插件的属性,并一个一个向外传递。
转载自:https://juejin.cn/post/7124568391326629901