# 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>
1
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>
1
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>
1
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>
1
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>
1
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>
1

: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>
1
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>
1
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>
1
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>
1
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>
1
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?表示该值可空,可不传
}
1
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>
1
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>
1
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 }]
});
1
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>
1
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>
1
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>
1
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>
1
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>
1
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
1
2
3

# 2 创建并引入mitt

可以在 src/utils 中创建一个 emitter.ts 文件,文件名称自定义。

编写内容如下:

import mitt from "mitt";

const emitter = mitt(); 

// 导出
export default emitter;
1
2
3
4
5
6

使用 mitt() 创建一个全局的事件总线,以便在整个应用中使用。


引入 emitter.ts 文件

在项目 main.ts 文件中,导入上面的 src/utils/emitter.ts 文件。

import emitter from './utils/emitter'
1

导入即可。

下面就可以开始使用 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>
1
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>
1
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.vueChildCom.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>
1
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>
1
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>
1
2
3
4
5
6
7
8
9

啥也没干,就是使用 v-modelattrs 传递给了 孙组件


在孙组件 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这样就实现了父组件与孙组件的传值,但是需要经过子组件过一手,是有一点麻烦。

没关系,下面再介绍一种父组件向后代组件传值的方法。

# 14.7 父组件与后代组件通信

下面介绍的是 provideinject,使用它们可以实现一个组件与其后代组件进行通信,可以与后代所有的组件通信。

举个栗子,我还是使用这样的结构:

HomePage.vue(父组件) -> ChildCom.vue (子组件) -> GrandChildCom.vue(孙子组件)

下面实现 HomePage.vueGrandChildCom.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>
1
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>
1
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>
1
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>
1
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>
1
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>
1
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>
1

可以简写为 v-model

<DoubiText v-model="username"></DoubiText>
1

所以第三方封装的 UI 组件是通过上面的方式来实现双向数据绑定的,了解一下原理。