# Vue3教程 - 14 组件
什么是组件?
前面在进行学习的时候,已经使用了组件,在 App.vue
组件中完成的,但是在项目中只使用了这一个组件,作为整个项目的根组件。
在正常的项目中,是将 App
作为根组件的,然后在 App
中通过路由(后面学习)来控制页面的切换,实际控制的是组件的切换;页面中展示的内容,也是封装成一个个组件,利于功能的划分和代码的复用。最终构成整个前端的 SPA(Single Page Application) 单页面应用。
下面来正式学习一下组件的使用。
# 14.1 创建组件
现在我们来创建一个组件,然后在 App.vue
根组件中引入并使用这个组件。
# 1 创建组件
在 src
目录下创建一个 components
目录,然后在 components
目录下创建一个 HomePage.vue
文件,后缀名为 .vue
。
在 HomePage.vue
文件中输入如下内容:
<template>
<h1>HomePage组件</h1>
</template>
<!-- setup -->
<script lang="ts" setup>
</script>
<style scoped>
</style>
2
3
4
5
6
7
8
9
10
11
组件分为三个部分,在 HelloWorld
中已经讲解过了。
# 2 引入和使用组件
现在在 App.vue
页面引入上面创建的组件,使用 import
进行引入:
<template>
<div>
<HomePage></HomePage>
</div>
</template>
<script lang="ts" setup>
import HomePage from '@/pages/HomePage.vue';
</script>
2
3
4
5
6
7
8
9
10
- 首先使用
import
引入组件,路径中的@
表示src
目录; - 然后就可以使用组件了:
<HomePage />
。
显示效果如下:
# 14.5 组件的切换
这里的切换,是某一个页面中一个部件的切换,在 Vue 中,页面也是组件,但是页面之间的切换,是通过后面的路由来完成的,路由后面再讲解。
例如在登录页面,有两个按钮登录和注册,当点击登录按钮,显示输入登录信息的输入框,点击注册,在同样的位置显示填写注册的信息,其实这是一个登录组件和注册组件的切换,那么如何实现组件的切换呢?
# 1 通过v-if实现
通过在Vue实例中定义一个flag,在组件上通过 v-if
来获取 flag
的值来判断组件要不要显示,当点击登录或注册按钮的时候,修改flag的值。
定义两个组件
LoginCom.vue
<template>
<div id="root">
登录
</div>
</template>
<!-- setup -->
<script lang="ts" setup>
</script>
<style scoped>
#root {
width: 200px;
height: 200px;
background-color: red;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
RegisterCom.vue
<template>
<div id="root">
注册
</div>
</template>
<!-- setup -->
<script lang="ts" setup>
</script>
<style scoped>
#root {
width: 200px;
height: 200px;
background-color: yellowgreen;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
实现切换
在 HomePage.vue
中引入两个组件,通过定义 flag
属性,使用 v-if
来实现隐藏和显示。
<template>
<div>
<!-- 点击的时候,修改flag的值 -->
<a href="" @click.prevent="flag = !flag">切换</a>
<!-- 通过flag的值来判断显示哪个组件 -->
<LoginCom v-if="flag"></LoginCom>
<RegisterCom v-else></RegisterCom>
</div>
</template>
<script setup lang="ts">
import LoginCom from '@/components/LoginCom.vue'
import RegisterCom from '@/components/RegisterCom.vue'
import { ref } from 'vue';
let flag = ref(true);
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
效果如下:
# 2 使用component标签
使用 <component>
标签来实现切换。
<component :is="componentName"></component>
:is
属性指定的 componentName
是组件的名称,只要这个值是哪个组件,则在这个标签的位置就显示哪个组件。
这样我们在 setup
中定义一个变量用来保存显示的组件的名称,当点击登录
或注册
按钮的时候,修改这个变量的值就可以了。
HomePage.vue
代码:
<template>
<div>
<!-- 点击的时候,修改componentName的值 -->
<a href="" @click.prevent="componentName = LoginCom">登录</a>
<a href="" @click.prevent="componentName = RegisterCom">注册</a>
<!-- component标签 是一个占位符, :is属性,可以用来指定要展示的组件的名称 -->
<component :is="componentName"></component>
</div>
</template>
<script setup lang="ts">
// 导入组件
import LoginCom from '@/components/LoginCom.vue';
import RegisterCom from '@/components/RegisterCom.vue';
import { ref } from 'vue';
let componentName = ref(LoginCom);
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
当点击两个按钮的时候,通过修改 componentName
的值,使用 <component>
实现了组件的切换。
# 14.6 组件切换的动画
组件的切换很简单,只需要将 <component>
标签使用 <Transition>
标签包裹,然后在编写动画过渡即可。
在 <Transition>
上可以设置切换的模式,out-in
表示第一组件出去以后,第二个组件才能进来,不会同时看到两个组件。
HomeVue.vue
<template>
<div>
<!-- 点击的时候,修改componentName的值 -->
<a href="" @click.prevent="componentName = LoginCom">登录</a>
<a href="" @click.prevent="componentName = RegisterCom">注册</a>
<!-- component标签 是一个占位符, :is属性,可以用来指定要展示的组件的名称 -->
<!-- 通过 mode 属性,设置组件切换时候的 模式 -->
<Transition mode="out-in">
<component :is="componentName"></component>
</Transition>
</div>
</template>
<script setup lang="ts">
// 导入组件
import LoginCom from '@/components/LoginCom.vue';
import RegisterCom from '@/components/RegisterCom.vue';
import { ref } from 'vue';
let componentName = ref(LoginCom);
</script>
<style scoped>
.v-enter-from,
.v-leave-to {
opacity: 0;
transform: translateX(150px);
}
.v-enter-active,
.v-leave-active {
transition: all 0.5s ease;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
mode 的默认值是 mode="in-out"
:同时开始进入和离开的过渡,但在实际的动画效果中,可能会看到新旧元素的重叠。
# 14.2 父组件向子组件传值
在子组件中无法访问父组件中定义的数据和方法。
那么父子组件如何进行数据传递呢?
父组件通过属性绑定的方式给子组件传值,子组件通过 defineProps
接收传递的值。
举个栗子:
我们再定义一个子组件 ChildCom.vue
然后在 HomePage.vue
组件中引入并使用,并传递参数。
从 HomePage.vue
组件向子组件 ChildCom.vue
传值。
# 1.2.1 父组件传递值
父组件 HomePage.vue
在父组件通过 v-bind:属性
或 :属性
给子组件传值。
<template>
<!-- 给子组件传值 -->
<ChildCom :name="'Doubi'" :age="age"></ChildCom>
<!-- 修改传递的值 -->
<button @click="changeAge">改变父组件的Msg</button>
</template>
<!-- setup -->
<script lang="ts" setup>
import ChildCom from '@/components/ChildCom.vue';
import { ref } from 'vue';
let age = ref(13);
function changeAge() {
age.value = 14;
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
同时在父组件修改组件中,通过按钮修改父组件的数据。
# 1.2.2 子组件接收值
子组件 ChildCom.vue
:
在子组件中,通过 defineProps
接收值。
<template>
<div>
<span>父组件传递的值:{{ name }}, {{ age }}</span>
</div>
</template>
<script lang="ts" setup>
// 接收父组件的传值
defineProps(['name', 'age']); // 接收值
</script>
2
3
4
5
6
7
8
9
10
11
12
defineProps
的参数是一个数组,里面是从父组件接收的属性,即使是一个参数,也需要使用数组接收。- 当父组件中的值发生变化,会同步更新到子组件中。
- 父组件传递给子组件的数据,对于子组件而言是只读的,子组件无法修改。
显示效果如下:
在上面的代码中,没有直接定义变量接收父组件传递的值,其实 defineProps()
是有返回参数的,是一个对象,对象中包含了传递的参数:
<script lang="ts" setup>
// 接收父组件的传值
let props = defineProps(['name', 'age']); // 接收值
console.log("name:", props.name); // doubi
console.log("age:", props.age) // 13
</script>
2
3
4
5
6
7
8
这样可以在代码中获取传递的值。
# 1.2.3 传值约束
Vue3 是拥抱 Typescript 的,所以我们在传值的时候,可以添加类型约束,避免传递不符合要求的数据。
举个例子:
# 1 定义数据类型
先定义一个 IPerson 类型的数据。
在 src/types/index.ts
文件中(没有就创建)定义 IPerson 的接口类型。
export interface IPerson {
id: string,
username: string,
age?: number // age?表示该值可空,可不传
}
2
3
4
5
# 2 子组件约束传值类型
子组件可以规定传值的类型,那么父组件必须按照规定的类型来传值,否则报错:
ChildCom.vue
<template>
<div>{{ person.username }} - {{ person.age }}</div>
<ul>
<li v-for="(p, index) in list" :key="p.id">
Id:{{ p.id }} --- 名字:{{ p.username }} --- 索引:{{ index }}
</li>
</ul>
</template>
<!-- setup -->
<script lang="ts" setup>
// 引入IPerson接口
import { type IPerson } from '@/types';
defineProps<{person:IPerson, list:IPerson[]}>();
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 首先使用
import { type IPerson } from '@/types';
引入类数据类型; - 然后使用
defineProps<{person:IPerson, list:IPerson[]}>();
定义了接收数据的属性名称和类型,这样传值的人不需按照这个类型来传值,上面是定义了IPerson
对象类型和IPerson
类型的数组。
# 3 父组件传值
HomePage.vue
<template>
<!-- 需要按照子组件的规定传值-->
<ChildCom :person="person" :list="personList" />
</template>
<!-- setup -->
<script lang="ts" setup>
import ChildCom from '@/components/ChildCom.vue';
import { reactive, ref } from 'vue';
// 引入IPerson接口
import { type IPerson } from '@/types';
// 定义对象类型
let person = ref<IPerson>({ id: '001', username: 'doubi', age: 13 })
// 定义对象类型的数组
let personList = reactive<IPerson[]>([
{ id: '002', username: 'niubi', age: 14 },
{ id: '003', username: 'erbi', age: 15 },
{ id: '004', username: 'shibi', age: 16 }
])
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 在父组件中也首先引入了
IPerson
接口类型; - 然后定了两个数据,一个是
IPerson
类型的对象,和IPerson
类型的数组,并将其传递给子组件。
最终运行结果:
# 4 子组件设置默认值
子组件还可以设置让父组件可传值,也可以不传值,如果不传值,还可以设置默认值。
// 接收值,通过?表示可空,就是可以不传值,person没有使用?,则必须传值
// 通过withDefaults的第二个参数指定默认值,默认值是一个对象类型,对象中的属性是接收的参数,每个属性通过函数返回默认值
withDefaults(defineProps<{person:IPerson, list?:IPerson[]}>(), {
list: () => [{ id: '100', username: 'haha', age: 14 }]
});
2
3
4
5
# 14.3 子组件向父组件传值
子组件向父组件传值是通过方法回调的方式实现的,在父组件通过 v-bind:属性
或 :属性
给子组件传递一个函数,子组件调用传递的函数,将数据作为函数的参数来实现数据的传递。
举个栗子:
下面的例子中,父组件将函数传递给子组件,通过点击子组件的按钮,来触发父组件传递的函数,将子组件自己的私有数据传递给父组件。
# 1 父组件传递函数
父组件 HomePage.vue
:
<template>
<div>{{ msg }}</div>
<!-- 3.使用子组件 -->
<ChildCom :childClick="changeMsg"></ChildCom>
</template>
<!-- setup -->
<script lang="ts" setup>
import ChildCom from '@/components/ChildCom.vue';
import { ref } from 'vue';
let msg = ref('')
// 通过子组件调用父组件的方法,修改父组件中的数据
function changeMsg(childData: string) {
msg.value = childData;
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
父组件在使用子组件的时候,通过 :childClick="changeMsg"
将函数 changeMsg
绑定到指令 childClick
上,在子组件中就可以通过 childClick
调用父组件的函数了。
# 2 子组件回调函数
子组件 ChildCome.vue
:
点击按钮后,通过调用父组件方法,传递数据给父组件。
<template>
<div>
<button @click="doClick">把子组件的数据传递给父组件</button>
</div>
</template>
<!-- setup -->
<script lang="ts" setup>
let props = defineProps(['childClick']);
function doClick() {
props.childClick('Hello Doubi'); // 调用父组件的函数
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用 defineProps
接收参数和接收函数是一样的,通过 defineProps
返回的对象,调用父组件传递的函数。
也可以直接在按钮上调用:
<template>
<div>
<!-- 直接调用 -->
<button @click="childClick('Hello Doubi')">把子组件的数据传递给父组件</button>
</div>
</template>
<!-- setup -->
<script lang="ts" setup>
defineProps(['childClick']);
</script>
2
3
4
5
6
7
8
9
10
11
12
13
# 14.4 自定义事件父传值给子
通过自定义事件,也可以实现子组件像父组件传递参数。
在上面子组件向父组件传递参数是通过 v-bind:属性
或 :属性
给子组件传递一个函数,还可以通过自定义事件,将父组件的函数传递给子组件,子组件调用函数传递参数给父组件。思路差不多,都是传递函数。
举个栗子:
# 1 父组件传递函数
父组件 HomePage.vue
:
和上面通过 v-bind:属性
或 :属性
给子组件传递一个函数一样,只是改成了 @事件
。
<template>
<div>{{ msg }}</div>
<!-- 3.使用子组件 -->
<ChildCom @childClick="changeMsg"></ChildCom>
</template>
<!-- setup -->
<script lang="ts" setup>
import ChildCom from '@/components/ChildCom.vue';
import { ref } from 'vue';
let msg = ref('')
// 通过子组件调用父组件的方法,修改父组件中的数据
function changeMsg(childData: string) {
msg.value = childData;
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
父组件在使用子组件的时候,通过 @childClick="changeMsg"
将函数 changeMsg
绑定到事件 childClick
上,在子组件中就可以通过 childClick
调用父组件的函数了。
# 2 子组件回调函数
子组件 ChildCome.vue
:
点击按钮后,通过调用父组件方法,传递数据给父组件。
<template>
<div>
<button @click="doClick">把子组件的数据传递给父组件</button>
</div>
</template>
<!-- setup -->
<script lang="ts" setup>
let emits = defineEmits(['childClick']);
function doClick() {
emits('childClick', 'Hello Doubi'); // 调用父组件的函数
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用 defineEmits
获取事件,然后点击按钮的时候,通过 emits
触发事件,来调用父组件的函数。第一个参数是事件名称,第二个参数是参数。
# 14.5 mitt组件之间通信
mitt
是一个第三方的工具,可以实现任意两个组件之间的传值,是一个轻量级的事件总线库。mitt
的大小只有约200字节,使用起来也非常的简单,不会给应用带来过重的负担。
主要的思路是一个组件绑定事件,另一个组件触发事件并传递参数,这样就实现了组件之间的传值。
下面介绍一下如何使用。
# 1 安装
mitt
是一个第三方的工具,所以使用前需要先安装。
npm install mitt
# 或者
yarn add mitt
2
3
# 2 创建并引入mitt
可以在 src/utils
中创建一个 emitter.ts
文件,文件名称自定义。
编写内容如下:
import mitt from "mitt";
const emitter = mitt();
// 导出
export default emitter;
2
3
4
5
6
使用 mitt()
创建一个全局的事件总线,以便在整个应用中使用。
引入 emitter.ts
文件
在项目 main.ts
文件中,导入上面的 src/utils/emitter.ts
文件。
import emitter from './utils/emitter'
导入即可。
下面就可以开始使用 mitt
了,还是实现从 ChildCom.vue
组件向 HomePage.vue
组件传值。
# 3 绑定时间并接受参数
因为要实现从 ChildCom.vue
组件向 HomePage.vue
组件传值,所以需要在 HomePage.vue
组件中绑定事件,在 ChildCom.vue
组件中触发事件并传值。
编写 HomePage.vue
组件如下:
<template>
<div>{{ msg }}</div>
<ChildCom></ChildCom>
</template>
<!-- setup -->
<script lang="ts" setup>
import ChildCom from '@/components/ChildCom.vue';
import { ref , onBeforeUnmount} from 'vue';
import emitter from '@/utils/emitter';
let msg = ref('')
// 绑定事件
emitter.on('send-msg', (value: any) => {
msg.value = value;
});
//建议在组件卸载之前解绑,防止内存泄漏
onBeforeUnmount(() => {
emitter.off('send-msg');
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
引入 emitter
,然后在 emitter
上绑定事件,第一个参数是事件名称,触发的时候使用,第二个参数是一个函数,触发事件时候的回调函数,函数的参数用来接收参数。
建议在组件卸载的时候解绑事件,避免内存泄漏。
**mitt 可以实现任意两个组件传值。**只是恰巧这里 ChildCom.vue
组件 HomePage.vue
组件是父子关系。
# 4 触发事件并传递参数
编写 ChildCom.vue
组件,在 ChildCom.vue
组件中触发事件并传递参数。
如下:
<template>
<div>
<button @click="doClick">把子组件的数据传递给父组件</button>
</div>
</template>
<!-- setup -->
<script lang="ts" setup>
import emitter from '@/utils/emitter';
function doClick() {
emitter.emit('send-msg', 'Hello Doubi');
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
引入 emitter
,然后使用 emitter.emit()
,触发事件,指定事件的名称和参数即可, 这样 HomePage.vue
组件就可以接收到参数了。
# 14.6 父组件与孙组件通信attrs
下面介绍一下父组件和孙子组件如何相互传值,当然通过 mitt
也是可以实现的。
结构是这样的:
HomePage.vue
(父组件) -> ChildCom.vue
(子组件) -> GrandChildCom.vue
(孙子组件)
先重新看一下父组件给子组件传值,也就是HomePage.vue
给 ChildCom.vue
(子组件)传值,子组件 ChildCom.vue
不接收的情况下,会发生什么。
HomePage.vue
<template>
<ChildCom :msg1="'Hello'" :msg2="msg" :clickChange="changeMsg" ></ChildCom>
</template>
<!-- setup -->
<script lang="ts" setup>
import ChildCom from '@/components/ChildCom.vue';
import { ref } from 'vue';
let msg = ref('Doubi')
function changeMsg(value: string) {
msg.value = value
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在上面的代码中,父组件给子组件传递了2个值和1个函数。
ChildCom.vue
<template>
<div>
{{ msg1 }}
</div>
</template>
<!-- setup -->
<script lang="ts" setup>
defineProps(['msg1'])
</script>
2
3
4
5
6
7
8
9
10
11
12
在子组件中只接受了 msg1
参数,在浏览器中通过 vue 插件查看,可以看到子组件接收的参数在 props
中,没有接收的参数在 attrs
中:
所以这里实现的方式,就是将 attrs
传递给 孙组件
。
所以修改 ChildCom.vue
如下:
<template>
<GrandChildCom v-model="$attrs"></GrandChildCom>
</template>
<!-- setup -->
<script lang="ts" setup>
import GrandChildCom from './GrandChildCom.vue';
</script>
2
3
4
5
6
7
8
9
啥也没干,就是使用 v-model
将 attrs
传递给了 孙组件
。
在孙组件 GrandChildCom.vue
中获取传递的属性和函数,并可以调用函数,向父组件 HomePage.vue
传递参数。
<template>
<div>
{{ msg1 }}
{{ msg2 }}
<br/>
<button @click="doClick">传给爷爷</button>
</div>
</template>
<!-- setup -->
<script lang="ts" setup>
let props = defineProps(['msg1', 'msg2', 'clickChange'])
function doClick() {
props.clickChange('Niubi'); // 调用爷爷组件的函数
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这样就实现了父组件与孙组件的传值,但是需要经过子组件过一手,是有一点麻烦。
没关系,下面再介绍一种父组件向后代组件传值的方法。
# 14.7 父组件与后代组件通信
下面介绍的是 provide
和 inject
,使用它们可以实现一个组件与其后代组件进行通信,可以与后代所有的组件通信。
举个栗子,我还是使用这样的结构:
HomePage.vue
(父组件) -> ChildCom.vue
(子组件) -> GrandChildCom.vue
(孙子组件)
下面实现 HomePage.vue
向 GrandChildCom.vue
组件传值,如果想在 ChildCom.vue
获取值,方式也是一样的。
# 1 父组件提供值
HomePage.vue
:
<template>
<div>{{ msg }}</div>
<ChildCom></ChildCom>
</template>
<!-- setup -->
<script lang="ts" setup>
import ChildCom from '@/components/ChildCom.vue';
import { ref, provide } from 'vue';
let msg = ref('Hello');
// 定义对象
let person = ref({'name': 'Doubi', 'age': 13});
// 定义函数
function changeData(value: string) {
msg.value = value;
}
provide('msg', msg);
provide('person', person);
provide('changeData', changeData);
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在父组件中,定义了普通类型数据、对象类型数据、还有一个函数,通过 provide()
函数提供给其后代组件。
# 2 子组件
子组件没什么好说的,这里只是引入孙子组件:
ChildCom.vue
:
<template>
<GrandChildCom></GrandChildCom>
</template>
<!-- setup -->
<script lang="ts" setup>
import GrandChildCom from './GrandChildCom.vue';
</script>
2
3
4
5
6
7
8
9
10
# 3 孙子组件获取值
下面看一下如何在孙子组件中获取父组件的数据和函数。
GrandChildCom.vue
:
<template>
<div>
<hr/>
<div>孙子组件</div>
<div>{{ msg }}</div>
<div>{{ person.name }} - {{ person.age }}</div>
<button @click="changeParentData">修改爷组件的值</button>
</div>
</template>
<!-- setup -->
<script lang="ts" setup>
import { inject } from 'vue';
let msg = inject('msg');
let person = inject('person', {'name': '', 'age': 0}); // 第二个参数可以提供默认值
let changeFunc = inject('changeData', (value: string) => {});
function changeParentData() {
changeFunc('Are you ok ?')
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 在孙子组件中通过
inject()
可以获取到父组件传递的数据和函数; inject()
第二个参数可以提供一个默认值,如果获取不到则使用默认值;- TS 在这里推断不出传递对象的属性和函数的类型和函数的参数,所以可以使用默认值帮助其推断,避免语法检查报错;
这样在孙子组件就可以获取到父组件传的数据,并可以调用父组件的函数修改父组件的数据。其实父组件所有的后代组件都可以向上面一样获取到数据。
# 14.8 组件的v-model指令
这个小节在实际的开发中,一般不会用到,但是能帮你理解一些 UI 组件库的实现原理。
# 1 v-model的实现原理
在前面我们讲解了 v-model
指令可以实现表单输入框的双向数据绑定。其实 v-model
指令是通过 :value
属性绑定和事件绑定实现的。
<template>
<div>
<input type="text" v-model="username" />
<input type="text" :value="username" @input="username = (<HTMLInputElement>$event.target).value"/>
</div>
</template>
<!-- setup -->
<script lang="ts" setup>
import { ref } from 'vue';
let username = ref('');
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上面在 input
标签上双向绑定了 username
。
在第二个标签中:
:value="username"
是从 Vue 中读取数据;@input="username = (<HTMLInputElement>$event.target).value"
是将文本框的内容传递给 Vue,(<HTMLInputElement>$event.target)
是类型转换,其实就是$event.target.value
获取文本框的值。
所以 v-model="username"
其实是使用第二种方式来实现的。
但是 v-model
指令只能在 HTML 标签上实现双向数据绑定,但是我们在使用一些第三方的组件库时,例如使用第三方封装的文本框,为什么也可以使用 v-model
指令来实现双向数据绑定呢?
其实也是通过父组件向组件传值和传递事件来实现的。
# 2 自定义文本框组件
下面我们自定义一个文本,实现双向数据绑定,就像使用第三方封装的组件库一样。
假设定义了子组件 DoubiText
,然后在父组件中使用。
<template>
<DoubiText :modelValue="username" @update:modelValue="username = $event"></DoubiText>
</template>
<!-- setup -->
<script lang="ts" setup>
import DoubiText from './DoubiText.vue';
import { ref } from 'vue';
let username = ref('123');
</script>
2
3
4
5
6
7
8
9
10
11
12
在上面给 DoubiText
组件传递了属性 modelValue
和事件 update:modelValue
。
那么就可以定义 DoubiText
组件,并接收属性和事件:
DoubiText.vue
:
<template>
<input type="text" :value="modelValue" @input="emit('update:modelValue', (<HTMLInputElement>$event.target).value)" />
</template>
<!-- setup -->
<script lang="ts" setup>
// 接收属性
defineProps(['modelValue']);
// 接收事件
const emit = defineEmits(['update:modelValue']);
</script>
2
3
4
5
6
7
8
9
10
11
12
13
接收到属性和事件,然后绑定到 input
标签上,这样 input 标签就可以读取到值了,同时输入事件会触发调用事件,将值传递给父组件,父组件在上面使用 username = $event
接收,这样就实现了和父组件中的数据双向绑定。
- 对于原生标签,
$event
就是事件对象,所以$event.target
获取到标签元素; - 对于自定义组件,
$event
就是触发事件时传递的数据。
下面这样的写法很繁琐:
<DoubiText :modelValue="username" @update:modelValue="username = $event"></DoubiText>
可以简写为 v-model
:
<DoubiText v-model="username"></DoubiText>
所以第三方封装的 UI 组件是通过上面的方式来实现双向数据绑定的,了解一下原理。
← 13-动画 15-ref与$parent →