完整原文地址见简书https://cloud.tencent.com/developer/article/1805447
本文内容提要
- 父子组件可通过事件 进行通信
- 携带参数的事件 发送和监听回调
- 使用 组件的**
emits
**板块 整理组件事件 - 使用 组件**
emits
**板块的Object
**形式 校验外传的参数值** - 结合**
$emit
**、**v-bind
**与**v-model
** 实现 父子组件通信(数据双向绑定) - 结合**
$emit
**、**v-bind
**与**v-model
** 实现 父子组件通信(多个字段的应用案例) - 自定义修饰符
- **实验****`this.modelModifiers`****的作用**
代码语言:txt复制- **常规的利用****`双向绑定特性`****,通过点击事件切换UI的写法**
- **动态组件写法**
父子组件可通过事件 进行通信
前面的笔记 —— 《Vue3 | 组件的定义及复用性、局部组件、全局组件、组件间传值及其校验、单项数据流、Non-props属性》,单向数据流的概念,
即子组件无法修改来自父组件的数据字段,
如果确要修改,可以使用下面说的方式进行通信:
首先,在子组件的UI点击回调方法中,调用**this.$emit('【自定义事件名】')
**,
向外发送一个**事件
**;
接着各级父组件会收到这个事件,
则在父组件中 调用 子组件标签处,
以 @【事件名】= "回调方法名"
**的形式,**监听
**该事件以及配置**回调方法
**;**
回调方法
**中即可 对 子组件意图修改 的 父组件数据字段 进行修改;**
注意, 触发事件的命名,用**
驼峰命名法
**(如下heHeDa); 监听事件的命名,用**横杆间隔法(如下he-he-da)
**。
代码:
代码语言:javascript复制<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World! heheheheheheda</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="heheApp"></div>
</body>
<script>
const app = Vue.createApp({
data() {
return {count: 1}
},
methods: {
handleItemEvent() {
this.count = 1;
}
},
template: `
<div>
<counter :count="count" @he-he-da="handleItemEvent"/>
</div>`
});
app.component('counter', {
props: ['count'],
methods: {
handleItemClick() {
this.$emit('heHeDa');
}
},
template:`
<div @click="handleItemClick">{{count}}</div>
`
});
const vm = app.mount('#heheApp');
</script>
</html>
运行,点击组件:
携带参数的事件 发送和监听回调
this.$emit()
**可以添加参数位,**
父组件的监听回调中,
则可加形参位 用于接收参数(如**handleItemEvent(param)
**中的 param
**);**
代码:
代码语言:javascript复制<script>
const app = Vue.createApp({
data() {
return {count: 1}
},
methods: {
handleItemEvent(param) {
this.count = param;
}
},
template: `
<div>
<counter :count="count" @add-count="handleItemEvent"/>
</div>`
});
app.component('counter', {
props: ['count'],
methods: {
handleItemClick() {
this.$emit('addCount', 8);
}
},
template:`
<div @click="handleItemClick">{{count}}</div>
`
});
const vm = app.mount('#heheApp');
</script>
运行,点击效果:
子组件需 发送多个参数 亦可,只要在**this.$emit()
**按需添加参数位,
父组件的监听回调中,添加对应的形参 去接收即可:
代码语言:javascript复制<script>
const app = Vue.createApp({
data() {
return {count: 1}
},
methods: {
handleItemEvent(param1, param2, param3) {
this.count = this.count param1 param2 param3;
console.log(this.count);
}
},
template: `
<div>
<counter :count="count" @add-count="handleItemEvent"/>
</div>`
});
app.component('counter', {
props: ['count'],
methods: {
handleItemClick() {
this.$emit('addCount', 8, 2, 6);
}
},
template:`
<div @click="handleItemClick">{{count}}</div>
`
});
const vm = app.mount('#heheApp');
</script>
效果:
当然 父组件 接收 子组件参数 后的 计算逻辑,
可以在 子组件传参 的时候 计算完成 再传给**this.$emit()
**!
父组件接收时,直接 受值即可(**handleItemEvent(count)
**);
<script>
const app = Vue.createApp({
data() {
return {count: 1}
},
methods: {
handleItemEvent(count) {
this.count = count;
console.log(this.count);
}
},
template: `
<div>
<counter :count="count" @add-count="handleItemEvent"/>
</div>`
});
app.component('counter', {
props: ['count'],
methods: {
handleItemClick() {
this.$emit('addCount', this.count 16);
}
},
template:`
<div @click="handleItemClick">{{count}}</div>
`
});
const vm = app.mount('#heheApp');
</script>
效果同上一个例子;
使用 组件的emits
板块 整理组件事件
实际开发场景中,我们一个组件自定义的触发事件可能会很多,
我们不可能一个一个去梳理核实,
这个时候就可以使用 组件的**emits
**板块 来整理组件的事件;
可以把组件中 自定义到的事件
**都写在这里,**方便梳理
**,提高**可读性
**,**
或者把 想要定义的事件
写在这里,
如此一来,如果**忘记
**编写对应的自定义事件,
Vue系统会在运行时 给予**警告
**:
<script>
const app = Vue.createApp({
data() {
return {count: 1}
},
methods: {
handleItemEvent(count) {
this.count = count;
console.log(this.count);
}
},
template: `
<div>
<counter :count="count" @add-count="handleItemEvent"/>
</div>`
});
app.component('counter', {
props: ['count'],
emits: ['hehehe'],
methods: {
handleItemClick() {
this.$emit('addCount', this.count 16);
}
},
template:`
<div @click="handleItemClick">{{count}}</div>
`
});
const vm = app.mount('#heheApp');
</script>
如果**忘记
**编写对应的自定义事件,Vue系统会在运行时 给予**警告
**:
使用 组件emits
板块的 Object
形式 校验外传的参数值
可以根据需要,使用 组件**emits
**板块的 Object
**形式 校验外传的参数值,**
如下,子组件的**emits
**板块,
‘key’值定义对应的事件名,‘value’值定义一个校验函数,
返回**true
**表示同意数值外传,
返回**false
**表示不同意,会给出警告;
<script>
const app = Vue.createApp({
data() {
return {count: 1}
},
methods: {
handleItemEvent(count) {
this.count = count;
console.log(this.count);
}
},
template: `
<div>
<counter :count="count" @add-count="handleItemEvent"/>
</div>`
});
app.component('counter', {
props: ['count'],
emits: {
addCount: (count) => {
if (count < 0) {
return true;
}
return false;
}
},
methods: {
handleItemClick() {
this.$emit('addCount', this.count 16);
}
},
template:`
<div @click="handleItemClick">{{count}}</div>
`
});
const vm = app.mount('#heheApp');
</script>
运行,点击效果:
结合$emit
、v-bind
与v-model
实现 父子组件通信(数据双向绑定)
v-model可以实现数据字段与DOM节点内容的双向绑定, 也可以实现数据字段与数据字段之间的双向绑定; 而**
v-bind
**只能是实现**单向数据流
**;
若不自定义**承接的字段名
**,则需要用**modelValue
**作为默认的**承接字段名
**;
同时,**$emit()
**的一参默认为**update:modelValue
**,二参为绑定的数据;
如下代码,
子组件 的承接变量**modelValue
** 同父组件的**count
**字段 双向绑定,
(实际上就是**v-model
**的特性 —— 将 子组件的内容即**modelValue
** 同 父组件的**数据字段
**双向绑定)
然后显示在子组件的DOM中(**{{modelValue}}
**):
<script>
const app = Vue.createApp({
data() {
return {count: 1}
},
template: `
<counter v-model="count"/>`
});
app.component('counter', {
props: ['modelValue'],
methods: {
handleItemClick() {
this.$emit('update:modelValue', this.modelValue 16);
console.log(vm.$data.count);
}
},
template:`
<div @click="handleItemClick">{{modelValue}}</div>
`
});
const vm = app.mount('#heheApp');
</script>
效果:
当然也可以**自定义字段名
**,
这种方式需要给**v-model
**字段接一个字段名,
同时将这个字段名替代子组件中所有**modelValue
**的位置:
<script>
const app = Vue.createApp({
data() {
return {count: 1}
},
template: `
<counter v-model:testField="count"/>`
});
app.component('counter', {
props: ['testField'],
methods: {
handleItemClick() {
this.$emit('update:testField', this.testField 16);
console.log(vm.$data.count);
}
},
template:`
<div @click="handleItemClick">{{testField}}</div>
`
});
const vm = app.mount('#heheApp');
</script>
实现效果与上例相同;
结合$emit
、v-bind
与v-model
实现 父子组件通信(多个字段的应用案例)
如下代码,
父组件的**count
**与子组件承接的**testField
**字段,
父组件的**count1
**与子组件承接的**testField1
**字段,
分别实现了双向绑定:
代码语言:javascript复制<script>
const app = Vue.createApp({
data() {
return {
count: 1,
count1: 1
}
},
template: `
<counter v-model:testField="count" v-model:testField1="count1"/>`
});
app.component('counter', {
props: ['testField','testField1'],
methods: {
handleItemClick() {
this.$emit('update:testField', this.testField 16);
console.log("vm.$data.count", vm.$data.count);
},
handleItemClick1() {
this.$emit('update:testField1', this.testField1 8);
console.log("vm.$data.count1", vm.$data.count1);
}
},
template:`
<div @click="handleItemClick">{{testField}}</div>
<div @click="handleItemClick1">{{testField1}}</div>
`
});
const vm = app.mount('#heheApp');
</script>
效果:
自定义修饰符
机制:在父组件调用处,在**
v-model
**后 使用**自定义修饰符
**, 在**实现修饰符逻辑
**的地方,如点击事件中, 通过**this.modelModifiers.[自定义修饰符名]
**返回的**布尔值
**, 判断用户是否使用了修饰符, 进而分别对使用与否做相应的处理; 另外**'modelModifiers'
**板块中可以**指定默认值
**(下代码指定为一个空对象**{}
**);
实验this.modelModifiers
的作用
首先下面是一个空的处理,**'modelModifiers'
**板块中**指定默认值
**(下代码指定为一个空对象**{}
**),
mounted
**函数中打印 子组件**modelModifiers
**属性的内容,**
代码如下,
运行后,可以见打印了一个对象**{captalize: true}
**,
正是我们传入的自定义修饰符**.captalize
**(这里未做处理)
【如果这里**v-model
**不接修饰符,
console.log(this.modelModifiers);
**将打印一个空对象**{}
**】:**
<script>
const app = Vue.createApp({
data() {
return {
char: 'a'
}
},
template: `
<counter v-model.captalize="char"/>`
});
app.component('counter', {
props: {
'modelValue': String,
'modelModifiers': {
default: () => ({})
}
},
mounted() {
console.log(this.modelModifiers);
},
methods: {
handleClick() {
this.$emit('update:modelValue', this.modelValue 'h');
console.log("vm.$data.count", vm.$data.char);
}
},
template:`
<div @click="handleClick">{{modelValue}}</div>
`
});
const vm = app.mount('#heheApp');
</script>
下面在子组件的点击回调handleClick()
中,通过this.modelModifiers.[自定义修饰符名]
实现自定义修饰符逻辑
实现效果即 点击之后使得对应的字符串 全变大写;
代码语言:javascript复制<script>
const app = Vue.createApp({
data() {
return {
testString: 'a'
}
},
template: `
<counter v-model.heheda="testString"/>`
});
app.component('counter', {
props: {
'modelValue': String,
'modelModifiers': {
default: () => ({})
}
},
mounted() {
console.log(this.modelModifiers);
},
methods: {
handleClick() {
let newValue = this.modelValue 'h';
if(this.modelModifiers.heheda) {
newValue = newValue.toUpperCase();
}
this.$emit('update:modelValue', newValue);
console.log("vm.$data.count", vm.$data.testString);
}
},
template:`
<div @click="handleClick">{{modelValue}}</div>
`
});
const vm = app.mount('#heheApp');
</script>
效果:
插槽【slot】【传组件示例】
代码语言:javascript复制使用关键 主要分两个部分: 自定义子组件: 在需要 被父组件**
插入组件
**的位置, 使用**<slot></slot>
**标签对临时占位; 父组件: 在调用**子组件标签对
**时, 往**子组件标签对
** 间 写上 要替换**子组件标签对
**中**<slot></slot>
**位置的组件 【slot】的出现, 方便父子组件之间数据的传递, 方便DOM的传递;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World! heheheheheheda</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="heheApp"></div>
</body>
<script>
const app = Vue.createApp({
template: `
<myform>
<div>提交</div>
</myform>
<myform>
<button>提交</button>
</myform>`
});
app.component('myform', {
template:`
<div>
<input />
<slot></slot>
<br><br>
</div>
`
});
const vm = app.mount('#heheApp');
</script>
</html>
运行效果:
注意,slot标签上是无法直接添加事件(修饰符)的,如有需要,可以在<slot>外层包裹一层<span>标签,再加上事件
代码语言:javascript复制<script>
const app = Vue.createApp({
template: `
<myform>
<div>提交</div>
</myform>
<myform>
<button>提交</button>
</myform>`
});
app.component('myform', {
methods: {
handleClick() {
console.log("heheda!!===");
}
},
template:`
<div>
<input />
<span @click="handleClick">
<slot></slot>
</span>
<br><br>
</div>
`
});
const vm = app.mount('#heheApp');
</script>
运行,点击提交文本或按钮:
插槽【传 字符串示例】
代码语言:javascript复制<script>
const app = Vue.createApp({
template: `
<myform>
66666
</myform>
<myform>
88888
</myform>`
});
app.component('myform', {
methods: {
handleClick() {
console.log("heheda!!===");
}
},
template:`
<div>
<input />
<span @click="handleClick">
<slot></slot>
</span>
<br><br>
</div>
`
});
const vm = app.mount('#heheApp');
</script>
插槽【传 自定义子组件 示例】
代码语言:javascript复制<script>
const app = Vue.createApp({
template: `
<myform>
<test />
</myform>
<myform>
88888
</myform>`
});
app.component('test', {
template: `<div>test component</div>`
})
app.component('myform', {
methods: {
handleClick() {
console.log("heheda!!===");
}
},
template:`
<div>
<input />
<span @click="handleClick">
<slot></slot>
</span>
<br><br>
</div>
`
});
const vm = app.mount('#heheApp');
</script>
运行:
插槽作用域问题
代码语言:javascript复制虽然,
父组件
中 往子组件
标签间 插入的组件 会替换子组件
的插槽位, 但是父组件
中 往子组件
标签间 插入的组件, 其所使用的数据字段,仍然是父组件
的,而非子组件
; 父组件的template中 调用的数据是 父组件中的 data; 子组件的template中 调用的数据是 子组件中的 data;
<script>
const app = Vue.createApp({
data() {
return {
text: '提交'
}
},
template: `
<myform>
<div>{{text}}</div>
</myform>
<myform>
<button>{{text}}</button>
</myform>`
});
app.component('myform', {
methods: {
handleClick() {
console.log("heheda!!===");
}
},
template:`
<div>
<input />
<span @click="handleClick">
<slot></slot>
</span>
<br><br>
</div>
`
});
const vm = app.mount('#heheApp');
</script>
插槽 UI默认值
代码语言:javascript复制可以在子组件的**
插槽<slot>标签
**间 编写**默认值
**, 如果父组件没有使用 组件 注入**插槽
**, 则对应位置 会显示**默认值
**:
<script>
const app = Vue.createApp({
data() {
return {
text: '提交'
}
},
template: `
<myform>
<div>{{text}}</div>
</myform>
<myform>
<button>{{text}}</button>
</myform>
<myform>
</myform>`
});
app.component('myform', {
methods: {
handleClick() {
console.log("heheda!!===");
}
},
template:`
<div>
<input />
<span @click="handleClick">
<slot>default value</slot>
</span>
<br><br>
</div>
`
});
const vm = app.mount('#heheApp');
</script>
效果:
插槽的灵活拆分与应用【具名插槽】
- 使得插槽的 父组件注入部分 和 子组件占位部分,能够更加灵活的布局,
可以通过**v-slot:[插槽名]
**来对一组插槽命名,
父组件定义之后 插槽名
**及其对应的**组件
**之后,**
子组件只需要在要占位的地方,
配合**name
**属性 使用对应命名的<slot>标签,
即可将对应的父组件插槽组件占用过来;
- 父组件 的插槽注入部分的组件,
需要用<template>标签组包裹起来,
使用**
v-slot:[插槽名]
**命名一组插槽; - 子组件使用**
<slot name="[插槽名]"></slot>
**的形式,进行插槽组件块的临时占用;
<script>
const app = Vue.createApp({
template: `
<layout>
<template v-slot:header>
<div>头部</div>
</template>
<template v-slot:footer>
<div>尾部</div>
</template>
</layout>`
});
app.component('layout', {
template:`
<div>
<slot name="header"></slot>
<div>content</div>
<slot name="footer"></slot>
</div>
`
});
const vm = app.mount('#heheApp');
</script>
效果:
v-slot
指令的简写
v-slot:[插槽名]
可以简写成 #[插槽名]
<script>
const app = Vue.createApp({
template: `
<layout>
<template #header>
<div>头部</div>
</template>
<template #footer>
<div>尾部</div>
</template>
</layout>`
});
app.component('layout', {
template:`
<div>
<slot name="header"></slot>
<div>content</div>
<slot name="footer"></slot>
</div>
`
});
const vm = app.mount('#heheApp');
</script>
实现的效果同上例;
普通的v-for
例子 进行 列表渲染
下面在子组件中,
使用v-for
指令 循环 子组件的数据,创建DOM组件:
<script>
const app = Vue.createApp({
template: `
<test-list />`
});
app.component('test-list', {
data(){
return {
list: ["heheda", "xixi" , "lueluelue"]
}
},
template:`
<div>
<div v-for="item in list">{{item}}</div>
</div>
`
});
const vm = app.mount('#heheApp');
</script>
运行效果:
v-for
结合v-bind
、v-slot
、<slot>
做列表渲染
作用:给**
数据
**由**子组件
**提供, 但**列表UI实现
** 由**父组件
**调用处提供, 类似于**回调接口
**的设计逻辑!!!
子组件使用**v-for
**循环获取数据,
每一轮迭代 获得的子项数据,
通过**v-bind
**设置到**占位的<slot>标签
**中,
父组件中,在引用的 子组件标签上,
使用**v-slot
**承接 子组件通过**v-bind
**传来的所有数据字段,
同时将这些字段打包成一个类似**JSONObject
**的**结构 字段
**,
并为这个**字段
** 指定一个**形参名
**(如下代码中的**mySlotProps
**);
【注意! 前面是, 使用**
v-slot
**命名父组件中 拟填充插槽的组件, 子组件在**<slot>标签
**上,通过**name=
**使用 父组件的命名,灵活填充插槽; 而这里是, 的**slot
**反而是起到了类似**props
**的作用,而非之前的命名组件作用!】
在 拟填充插槽
**的DOM组件中,**
使用方才 v-slot
**指定的形参,用于**开箱取数据
**:**
<script>
const app = Vue.createApp({
template: `
<test-list v-slot="mySlotProps">
<div>{{mySlotProps.item}}</div>
</test-list>`
});
app.component('test-list', {
data(){
return {
list: ["heheda", "xixi" , "lueluelue"]
}
},
template:`
<div>
<slot v-for="item in list" :item="item" />
</div>
`
});
const vm = app.mount('#heheApp');
</script>
运行效果同上例;
使用解构
概念进行简写
使用**v-slot="{item}"
**替代前面的**props
**的结构逻辑形式;
意义是,把**mySlotProps
**这个承接属性的字段,
里面的**item
**属性直接**解构 剥取
**出来,直接拿来用;
<script>
const app = Vue.createApp({
template: `
<test-list v-slot="{item}">
<div>{{item}}</div>
</test-list>`
});
app.component('test-list', {
data(){
return {
list: ["heheda", "xixi" , "lueluelue"]
}
},
template:`
<div>
<slot v-for="item in list" :item="item" />
</div>
`
});
const vm = app.mount('#heheApp');
</script>
运行效果同上例;
动态组件
常规的利用双向绑定特性
,通过点击事件切换UI的写法:
代码语言:javascript复制<script>
const app = Vue.createApp({
data() {
return {
currentItem: 'input-item'
}
},
methods: {
handlerClick() {
this.currentItem === 'input-item'?
this.currentItem = 'div-item': this.currentItem = 'input-item'
}
},
template: `
<input-item v-show="currentItem === 'input-item'" />
<div-item v-show="currentItem === 'div-item'" />
<button @click="handlerClick">切换DOM组件</button>`
});
app.component('input-item', {
template:`
<input />`
});
app.component('div-item', {
template:`<div>heheda</div>`
});
const vm = app.mount('#heheApp');
</script>
运行效果:
动态组件写法
- 语法:
一般在父组件中,
使用占位标签**
<component :is="[需显示的 子组件名]" />
**, 效果即 占位位置,会显示is
**属性 指定组件名的子组件;** 另外, 使用**<keep-alive>
**标签,包裹**<component :is="[需显示的 子组件名]" />
**, 可以是切换组件的时候,能够缓存组件的数据, 如一个有**输入数据
**的<input> 切换成一个其他组件 再切换 回来的时候, 可以保留一开始的**输入数据
**:
<script>
const app = Vue.createApp({
data() {
return {
currentItem: 'input-item'
}
},
methods: {
handlerClick() {
console.log("handlerClick ----- ");
this.currentItem === 'input-item'?
this.currentItem = 'div-item': this.currentItem = 'input-item'
}
},
template: `
<keep-alive>
<component :is="currentItem" />
</keep-alive>
<button @click="handlerClick">切换DOM组件</button>`
});
app.component('input-item', {
template:`
<input />`
});
app.component('div-item', {
template:`<div>heheda</div>`
});
const vm = app.mount('#heheApp');
</script>
运行效果:
初始为有输入数据的输入框:
点击切换为文本组件:
再次点击,切换为**有输入数据的输入框
**,
由于**<keep-alive>
**的作用,数据缓存下来,没有丢失,
如果没加**<keep-alive>
**,这里会是空的输入框:
异步组件
首先,
本文在此案例之前的所有案例,都是**同步组件
**,
即随即渲染,一个线程运行;
下面是**异步(自定义子)组件
**,
可以设定在某个时刻开始,延迟一个时延后,再执行渲染:
代码语言:javascript复制<script>
const app = Vue.createApp({
template: `
<div>
<div-item />
<my-async-item />
</div>`
});
app.component('div-item', {
template:`<div>heheda heheda</div>`
});
app.component('my-async-item', Vue.defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
template: `<div>this is my async component</div>`
})
}, 4000)
})
}))
const vm = app.mount('#heheApp');
</script>
关键代码【**异步(自定义子)组件
**】:
app.component('my-async-item', Vue.defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
template: `<div>this is my async component</div>`
})
}, 4000)
})
}))
运行效果: