组件基础
组件让我们能够将 UI 拆分为独立且可复用的部分,并以孤立的方式思考每一部分。一个应用通常会被组织成一个嵌套组件的树:

这与我们嵌套原生 HTML 元素的方式非常相似,但 Vue 实现了自己的组件模型,使我们能够在每个组件中封装自定义内容和逻辑。Vue 也能很好地与原生 Web Components 协同工作。如果你对 Vue 组件与原生 Web Components 之间的关系感兴趣,请在这里阅读更多。
定义组件
在使用构建步骤时,我们通常会在一个专门的文件中使用 .vue 扩展名来定义每个 Vue 组件——这被称为 单文件组件(简称 SFC):
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">你点击了我 {{ count }} 次。</button>
</template>在不使用构建步骤时,Vue 组件可以被定义为一个包含 Vue 特定选项的普通 JavaScript 对象:
js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">
你点击了我 {{ count }} 次。
</button>`
// 也可以指定一个 in-DOM 模板:
// template: '#my-template-element'
}这里的模板是以内联 JavaScript 字符串的形式写入的,Vue 会即时编译它。你也可以使用指向某个元素的 ID 选择器(通常是原生 <template> 元素)——Vue 会将其内容作为模板来源。
上面的示例定义了一个单独的组件,并将其作为 .js 文件的默认导出导出,但你也可以使用具名导出从同一个文件中导出多个组件。
使用组件
TIP
在本指南的其余部分,我们将使用 SFC 语法——无论你是否使用构建步骤,组件相关的概念都是相同的。示例 部分展示了这两种场景下的组件用法。
要使用子组件,我们需要先在父组件中导入它。假设我们把计数组件放在一个名为 ButtonCounter.vue 的文件中,该组件将作为该文件的默认导出暴露出来:
vue
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<h1>这里有一个子组件!</h1>
<ButtonCounter />
</template>使用 <script setup> 时,导入的组件会自动在模板中可用。
也可以全局注册组件,使其在给定应用中的所有组件里都可用,而无需导入它。全局注册与局部注册的优缺点会在专门的 组件注册 部分讨论。
组件可以按你需要的次数重复使用:
template
<h1>这里有很多子组件!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />注意,当点击按钮时,每个按钮都会维护自己独立的 count。这是因为每次使用组件时,都会创建一个新的实例。
在 SFC 中,建议子组件使用 PascalCase 标签名,以便与原生 HTML 元素区分开来。虽然原生 HTML 标签名是不区分大小写的,但 Vue SFC 是一种编译格式,因此我们可以在其中使用区分大小写的标签名。我们也可以使用 /> 来闭合标签。
如果你直接在 DOM 中编写模板(例如作为原生 <template> 元素的内容),那么模板将受到浏览器原生 HTML 解析行为的影响。在这种情况下,你需要为组件使用 kebab-case 和显式的结束标签:
template
<!-- 如果这个模板写在 DOM 中 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>更多细节请参见 in-DOM 模板解析注意事项。
传递 Props
如果我们正在构建一个博客,就很可能需要一个表示博客文章的组件。我们希望所有博客文章都共享相同的视觉布局,但内容不同。这样的组件只有在你能向它传递数据时才有用,比如我们想要显示的特定文章的标题和内容。这就是 props 的作用。
Props 是你可以在组件上注册的自定义属性。要向我们的博客文章组件传递标题,我们必须在该组件接受的 props 列表中声明它,使用 defineProps 宏:
vue
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>defineProps 是一个编译时宏,只能在 <script setup> 内部使用,并且不需要显式导入。已声明的 props 会自动暴露给模板。defineProps 还会返回一个对象,其中包含传递给组件的所有 props,因此我们在需要时可以在 JavaScript 中访问它们:
js
const props = defineProps(['title'])
console.log(props.title)另请参见:组件 Props 类型标注
如果你没有使用 <script setup>,则应使用 props 选项来声明 props,而 props 对象会作为第一个参数传递给 setup():
js
export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}一个组件可以拥有任意多个 props,默认情况下,任何值都可以传递给任何 prop。
一旦某个 prop 被注册,就可以像这样把数据作为自定义属性传给它:
template
<BlogPost title="我与 Vue 的旅程" />
<BlogPost title="使用 Vue 写博客" />
<BlogPost title="为什么 Vue 如此有趣" />不过,在典型应用中,你的父组件里很可能会有一个文章数组:
js
const posts = ref([
{ id: 1, title: '我与 Vue 的旅程' },
{ id: 2, title: '使用 Vue 写博客' },
{ id: 3, title: '为什么 Vue 如此有趣' }
])然后你会想要使用 v-for 为每一项渲染一个组件:
template
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>注意,传递动态 prop 值时使用了 v-bind 语法(:title="post.title")。当你事先不知道要渲染的确切内容时,这尤其有用。
关于 props,目前你只需要了解这些;但在你读完本页并对其内容感到熟悉之后,我们建议你之后再回来阅读关于 Props 的完整指南。
监听事件
随着我们开发 <BlogPost> 组件,某些功能可能需要向上传递给父组件。例如,我们可能会决定加入一个无障碍功能,让博客文章的文字变大,同时让页面其余部分保持默认大小。
在父组件中,我们可以通过添加一个 postFontSize ref 来支持这个功能:
js
const posts = ref([
/* ... */
])
const postFontSize = ref(1)它可以在模板中用于控制所有博客文章的字体大小:
template
<div :style="{ fontSize: postFontSize + 'em' }">
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
</div>现在让我们在 <BlogPost> 组件的模板中添加一个按钮:
vue
<!-- 省略 <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button>放大文字</button>
</div>
</template>这个按钮目前还没有任何作用——我们希望点击这个按钮时,通知父组件它应该放大所有文章的文字。为了解决这个问题,组件提供了一个自定义事件系统。父组件可以像监听原生 DOM 事件一样,使用 v-on 或 @ 监听子组件实例上的任意事件:
template
<BlogPost
...
@enlarge-text="postFontSize += 0.1"
/>然后子组件可以通过调用内置的 $emit 方法 并传入事件名,在自身上触发一个事件:
vue
<!-- 省略 <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">放大文字</button>
</div>
</template>多亏了 @enlarge-text="postFontSize += 0.1" 这个监听器,父组件会接收到该事件并更新 postFontSize 的值。
我们可以选择性地使用 defineEmits 宏 声明要触发的事件:
vue
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>这会记录组件会触发的所有事件,并且可以选择性地验证它们。它还允许 Vue 避免将这些事件隐式地作为原生监听器应用到子组件根元素上。
与 defineProps 类似,defineEmits 只能在 <script setup> 中使用,并且不需要导入。它会返回一个与 $emit 方法等价的 emit 函数。它可以用于在组件的 <script setup> 部分触发事件,因为在那里无法直接访问 $emit:
vue
<script setup>
const emit = defineEmits(['enlarge-text'])
emit('enlarge-text')
</script>另见:组件事件的类型标注
如果你没有使用 <script setup>,也可以使用 emits 选项声明要触发的事件。你可以通过 setup 上下文的一个属性访问 emit 函数(它作为第二个参数传入 setup()):
js
export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}到目前为止,你只需要了解这些自定义组件事件的内容。不过,在你读完这一页并对其内容感到熟悉之后,我们建议你之后再回来阅读完整的自定义事件指南。
使用插槽分发内容
就像 HTML 元素一样,能够向组件传递内容通常很有用,例如这样:
template
<AlertBox>
Something bad happened.
</AlertBox>它可能会渲染成这样:
这是一个用于演示目的的错误
Something bad happened.
这可以通过 Vue 的自定义 <slot> 元素来实现:
vue
<template>
<div class="alert-box">
<strong>这是一个用于演示目的的错误</strong>
<slot />
</div>
</template>
<style scoped>
.alert-box {
/* ... */
}
</style>正如上面所见,我们使用 <slot> 作为内容插入位置的占位符——就是这样。完成了!
到目前为止,你只需要了解这些插槽的内容。不过,在你读完这一页并对其内容感到熟悉之后,我们建议你之后再回来阅读完整的插槽指南。
动态组件
有时候,在多个组件之间动态切换会很有用,比如在一个标签页界面中:
上述功能可以通过 Vue 的带有特殊 is 属性的 <component> 元素来实现:
template
<!-- 当 currentTab 变化时,组件也会随之变化 -->
<component :is="tabs[currentTab]"></component>在上面的示例中,传给 :is 的值可以是以下两者之一:
- 已注册组件的名称字符串,或者
- 实际导入的组件对象
你也可以使用 is 属性来创建普通 HTML 元素。
在使用 <component :is="..."> 在多个组件之间切换时,被切换掉的组件会被卸载。我们可以使用内置的 <KeepAlive> 组件让非 सक्रिय组件保持“存活”。
DOM 内模板解析的注意事项
如果你直接在 DOM 中编写 Vue 模板,Vue 就需要从 DOM 中获取模板字符串。这会由于浏览器原生 HTML 解析行为而带来一些注意事项。
TIP
需要注意的是,下面讨论的限制只适用于你直接在 DOM 中编写模板的情况。若你使用的是来自以下来源的字符串模板,则不适用:
- 单文件组件
- 内联模板字符串(例如
template: '...') <script type="text/x-template">
大小写不敏感
HTML 标签和属性名对大小写不敏感,因此浏览器会把所有大写字符都解释为小写。这意味着,当你使用 DOM 内模板时,PascalCase 组件名和 camelCased 的 prop 名称或 v-on 事件名都需要使用它们对应的 kebab-case(连字符分隔)形式:
js
// JavaScript 中使用 camelCase
const BlogPost = {
props: ['postTitle'],
emits: ['updatePost'],
template: `
<h3>{{ postTitle }}</h3>
`
}template
<!-- HTML 中使用 kebab-case -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>自闭合标签
在前面的代码示例中,我们一直为组件使用自闭合标签:
template
<MyComponent />这是因为 Vue 的模板解析器会将 /> 视为任何标签结束的标记,而不管它的类型是什么。
不过,在 DOM 内模板中,我们始终必须写出显式的闭合标签:
template
<my-component></my-component>这是因为 HTML 规范只允许少数特定元素省略闭合标签,最常见的是 <input> 和 <img>。对于其他所有元素,如果你省略闭合标签,原生 HTML 解析器会认为你从未结束开始标签。例如,下面这段代码:
template
<my-component /> <!-- 我们本意是在这里关闭标签... -->
<span>hello</span>会被解析成:
template
<my-component>
<span>hello</span>
</my-component> <!-- 但浏览器会在这里关闭它。 -->元素位置限制
某些 HTML 元素,例如 <ul>、<ol>、<table> 和 <select>,对其内部可以出现的元素有限制;而某些元素,例如 <li>、<tr> 和 <option>,只能出现在某些特定的其他元素内部。
当组件与这些带有限制的元素一起使用时,就会引发问题。例如:
template
<table>
<blog-post-row></blog-post-row>
</table>自定义组件 <blog-post-row> 会被当作无效内容而提升到外层,导致最终渲染结果出错。我们可以使用特殊的 is 属性 作为变通方案:
template
<table>
<tr is="vue:blog-post-row"></tr>
</table>TIP
当 is 用在原生 HTML 元素上时,其值必须添加 vue: 前缀,才能被解释为 Vue 组件。这是为了避免与原生的 自定义内置元素 混淆。
到这里,你目前只需要了解这些关于 DOM 内模板解析注意事项的内容——事实上,这也就是 Vue 基础篇 的结束。恭喜你!还有很多内容值得学习,但首先,我们建议你先休息一下,自己动手玩一玩 Vue——做点有趣的东西,或者如果你还没看过,可以先看看一些示例。
当你对刚刚吸收的知识感到熟悉之后,就继续阅读本指南,深入了解组件的更多内容。