完整原文地址见简书https://www.jianshu.com/p/c8891bd3fe36
更多完整Vue笔记目录敬请见《前端 Web 笔记 汇总目录(Updating)》
本文内容提要
父子组件可通过事件 进行通信
携带参数的事件 发送和监听回调
使用 组件的
emits
板块 整理组件事件使用 组件
emits
板块的Object
形式 校验外传的参数值结合
$emit
、v-bind
与v-model
实现 父子组件通信(数据双向绑定)结合
$emit
、v-bind
与v-model
实现 父子组件通信(多个字段的应用案例)自定义修饰符
- 实验
this.modelModifiers
的作用下面在子组件的点击回调
handleClick()
中,通过this.modelModifiers.[自定义修饰符名]
实现自定义修饰符逻辑插槽【slot】【传组件示例】
注意,slot标签上是无法直接添加事件(修饰符)的,如有需要,可以在<slot>外层包裹一层<span>标签,再加上事件
插槽【传 字符串示例】
插槽【传 自定义子组件 示例】
插槽作用域问题
插槽 UI默认值
插槽的灵活拆分与应用【具名插槽】
v-slot
指令的简写普通的
v-for
例子 进行 列表渲染
v-for
结合v-bind
、v-slot
、<slot>
做列表渲染使用
解构
概念进行简写动态组件
- 常规的利用
双向绑定特性
,通过点击事件切换UI的写法- 动态组件写法
异步组件
父子组件可通过事件 进行通信
前面的笔记 —— 《Vue3 | 组件的定义及复用性、局部组件、全局组件、组件间传值及其校验、单项数据流、Non-props属性》,单向数据流的概念,
即子组件无法修改来自父组件的数据字段,
如果确要修改,可以使用下面说的方式进行通信:
首先,在子组件的UI点击回调方法中,调用this.$emit('【自定义事件名】')
,
向外发送一个事件
;
接着各级父组件会收到这个事件,
则在父组件中 调用 子组件标签处,
以 @【事件名】= "回调方法名"
的形式,监听
该事件以及配置回调方法
;
回调方法
中即可 对 子组件意图修改 的 父组件数据字段 进行修改;
注意,
触发事件的命名,用驼峰命名法
(如下heHeDa);
监听事件的命名,用横杆间隔法(如下he-he-da)
。
代码:
<!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
);
代码:
<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()
按需添加参数位,父组件的监听回调中,添加对应的形参 去接收即可:
<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
字段,
分别实现了双向绑定:
<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.[自定义修饰符名]
实现自定义修饰符逻辑
实现效果即 点击之后使得对应的字符串 全变大写;
<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】【传组件示例】
使用关键 主要分两个部分:
自定义子组件:
在需要 被父组件插入组件
的位置,
使用<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>标签,再加上事件
<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>
运行,点击提交文本或按钮:插槽【传 字符串示例】
<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>
插槽【传 自定义子组件 示例】
<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>
运行:插槽作用域问题
虽然,
父组件
中 往子组件
标签间 插入的组件 会替换子组件
的插槽位,
但是父组件
中 往子组件
标签间 插入的组件,
其所使用的数据字段,仍然是父组件
的,而非子组件
;
父组件的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默认值
可以在子组件的
插槽<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的写法:
<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>
,这里会是空的输入框:异步组件
首先,
本文在此案例之前的所有案例,都是同步组件
,
即随即渲染,一个线程运行;
下面是异步(自定义子)组件
,
可以设定在某个时刻开始,延迟一个时延后,再执行渲染:
<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)
})
}))
运行效果: